PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 All 41 releases
king-addons / includes / extensions / Theme_Builder / Theme_Builder.php

Theme_Builder.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.87, at includes/extensions/Theme_Builder/Theme_Builder.php

1,995 lines 76.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Theme Builder feature bootstrap.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Controls_Manager;
11 use Elementor\Plugin as Elementor_Plugin;
12 use King_Addons\Theme_Builder\Admin_List_Table;
13 use King_Addons\Theme_Builder\Conditions;
14 use King_Addons\Theme_Builder\Context;
15 use King_Addons\Theme_Builder\Meta_Keys;
16 use King_Addons\Theme_Builder\Repository;
17 use King_Addons\Theme_Builder\Resolver;
18 use King_Addons\Woo_Builder\Context as Woo_Context;
19 use WP_Post;
20
21 if (!defined('ABSPATH')) {
22 exit;
23 }
24
25 /**
26 * Registers Theme Builder feature and integrates with WordPress and Elementor.
27 */
28 class Theme_Builder
29 {
30 /**
31 * Admin menu slug.
32 *
33 * @var string
34 */
35 private string $menu_slug = 'king-addons-theme-builder';
36
37 /**
38 * Repository instance.
39 *
40 * @var Repository
41 */
42 private Repository $repository;
43
44 /**
45 * Resolver instance.
46 *
47 * @var Resolver
48 */
49 private Resolver $resolver;
50
51 /**
52 * Resolved template ID for current request.
53 *
54 * @var int|null
55 */
56 private ?int $current_template_id = null;
57
58 /**
59 * Constructor.
60 */
61 public function __construct()
62 {
63 $this->require_helpers();
64
65 $this->repository = new Repository();
66 $this->resolver = new Resolver($this->repository);
67
68 // Use priority 15 to ensure the parent menu exists before adding this submenu
69 add_action('admin_menu', [$this, 'register_admin_menu'], 15);
70 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
71 add_action('add_meta_boxes', [$this, 'register_meta_boxes']);
72 add_action('save_post', [$this, 'handle_save_post'], 10, 2);
73 add_action('delete_post', [$this, 'handle_delete_post']);
74 add_action('transition_post_status', [$this, 'handle_status_transition'], 10, 3);
75 add_action('admin_post_ka_theme_builder_create', [$this, 'handle_create_template']);
76 add_action('admin_post_ka_theme_builder_quick_update', [$this, 'handle_quick_update']);
77 add_action('admin_post_ka_theme_builder_duplicate', [$this, 'handle_duplicate_template']);
78 add_action('elementor/preview/enqueue_scripts', [$this, 'enqueue_preview_handler']);
79 add_action('elementor/documents/register_controls', [$this, 'register_document_controls']);
80 add_action('elementor/document/after_save', [$this, 'handle_document_after_save'], 10, 2);
81 add_filter('template_include', [$this, 'handle_template_include'], 99);
82 add_filter('king_addons/theme_builder/current_template_id', [$this, 'filter_current_template_id']);
83 add_filter('body_class', [$this, 'filter_body_class']);
84 }
85
86 /**
87 * Require helper files for Theme Builder.
88 *
89 * @return void
90 */
91 private function require_helpers(): void
92 {
93 require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Meta_Keys.php';
94 require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Context.php';
95 require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Conditions.php';
96 require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Repository.php';
97 require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Resolver.php';
98 require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Admin_List_Table.php';
99 }
100
101 /**
102 * Register admin menu entry.
103 *
104 * @return void
105 */
106 public function register_admin_menu(): void
107 {
108 add_menu_page(
109 esc_html__('Theme Builder', 'king-addons'),
110 esc_html__('Theme Builder', 'king-addons'),
111 'manage_options',
112 $this->menu_slug,
113 [$this, 'render_admin_page'],
114 'dashicons-layout',
115 54.6
116 );
117 }
118
119 /**
120 * Enqueue admin assets for Theme Builder pages.
121 *
122 * @param string $hook Current admin page hook.
123 *
124 * @return void
125 */
126 public function enqueue_admin_assets(string $hook): void
127 {
128 if (false === strpos($hook, $this->menu_slug)) {
129 return;
130 }
131
132 wp_enqueue_style(
133 KING_ADDONS_ASSETS_UNIQUE_KEY . '-theme-builder-admin',
134 KING_ADDONS_URL . 'includes/extensions/Theme_Builder/style.css',
135 [],
136 KING_ADDONS_VERSION
137 );
138
139 wp_enqueue_script(
140 KING_ADDONS_ASSETS_UNIQUE_KEY . '-theme-builder-admin',
141 KING_ADDONS_URL . 'includes/extensions/Theme_Builder/script.js',
142 ['jquery'],
143 KING_ADDONS_VERSION,
144 true
145 );
146 }
147
148 /**
149 * Register meta boxes for Elementor templates.
150 *
151 * @return void
152 */
153 public function register_meta_boxes(): void
154 {
155 add_meta_box(
156 'king-addons-theme-builder',
157 esc_html__('Theme Builder', 'king-addons'),
158 [$this, 'render_meta_box'],
159 'elementor_library',
160 'side',
161 'default'
162 );
163 }
164
165 /**
166 * Render Theme Builder meta box.
167 *
168 * @param WP_Post $post Current post.
169 *
170 * @return void
171 */
172 public function render_meta_box(WP_Post $post): void
173 {
174 wp_nonce_field('ka_theme_builder_meta_box', 'ka_theme_builder_meta_box_nonce');
175
176 $location = get_post_meta($post->ID, Meta_Keys::LOCATION, true);
177 $sub_location = get_post_meta($post->ID, Meta_Keys::SUB_LOCATION, true);
178 $priority = (int) get_post_meta($post->ID, Meta_Keys::PRIORITY, true);
179 $enabled = '1' === (string) get_post_meta($post->ID, Meta_Keys::ENABLED, true);
180
181 echo '<p><strong>' . esc_html__('Location', 'king-addons') . ':</strong> ' . esc_html($sub_location ?: '-') . '</p>';
182 echo '<p><strong>' . esc_html__('Type', 'king-addons') . ':</strong> ' . esc_html($location ?: '-') . '</p>';
183 echo '<p><strong>' . esc_html__('Priority', 'king-addons') . ':</strong> ' . esc_html((string) ($priority ?: 10)) . '</p>';
184 echo '<p><strong>' . esc_html__('Status', 'king-addons') . ':</strong> ' . ($enabled ? esc_html__('Enabled', 'king-addons') : esc_html__('Disabled', 'king-addons')) . '</p>';
185 echo '<p><a href="' . esc_url(admin_url('admin.php?page=' . $this->menu_slug)) . '">' . esc_html__('Manage in Theme Builder', 'king-addons') . '</a></p>';
186 }
187
188 /**
189 * Handle meta save for Elementor Library posts.
190 *
191 * @param int $post_id Post ID.
192 * @param WP_Post $post Post object.
193 *
194 * @return void
195 */
196 public function handle_save_post(int $post_id, WP_Post $post): void
197 {
198 if ('elementor_library' !== $post->post_type) {
199 return;
200 }
201
202 if (!isset($_POST['ka_theme_builder_meta_box_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['ka_theme_builder_meta_box_nonce'])), 'ka_theme_builder_meta_box')) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
203 return;
204 }
205
206 // Keep cache fresh if any relevant meta changes.
207 $this->repository->clear_cache();
208 }
209
210 /**
211 * Clear cache on delete.
212 *
213 * @return void
214 */
215 public function handle_delete_post(): void
216 {
217 $this->repository->clear_cache();
218 }
219
220 /**
221 * Clear cache on status transition.
222 *
223 * @param string $new_status New status.
224 * @param string $old_status Old status.
225 * @param WP_Post $post Post object.
226 *
227 * @return void
228 */
229 public function handle_status_transition(string $new_status, string $old_status, WP_Post $post): void
230 {
231 if ('elementor_library' !== $post->post_type || $new_status === $old_status) {
232 return;
233 }
234
235 $this->repository->clear_cache();
236 }
237
238 /**
239 * Render admin page for Theme Builder.
240 *
241 * @return void
242 */
243 public function render_admin_page(): void
244 {
245 if (!current_user_can('manage_options')) {
246 return;
247 }
248
249 // Keep cache fresh.
250 $this->repository->clear_cache();
251
252 // Shared dark theme support (used by Woo Builder admin page).
253 include KING_ADDONS_PATH . 'includes/admin/shared/dark-theme.php';
254
255 $this->handle_inline_actions();
256 $templates = $this->prepare_admin_templates();
257
258 $base_url = admin_url('admin.php?page=' . $this->menu_slug);
259 $status_filter = isset($_GET['status']) ? sanitize_key(wp_unslash($_GET['status'])) : 'all'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
260 if (!in_array($status_filter, ['all', 'enabled', 'disabled'], true)) {
261 $status_filter = 'all';
262 }
263 $filtered_templates = array_values(
264 array_filter(
265 $templates,
266 static function (array $template) use ($status_filter): bool {
267 if ('enabled' === $status_filter) {
268 return !empty($template['enabled']);
269 }
270 if ('disabled' === $status_filter) {
271 return empty($template['enabled']);
272 }
273 return true;
274 }
275 )
276 );
277
278 $type_cards = [
279 'single' => [
280 'label' => esc_html__('Single', 'king-addons'),
281 'desc' => esc_html__('Posts, pages & custom post types', 'king-addons'),
282 'default_location' => 'single_post',
283 ],
284 'archive' => [
285 'label' => esc_html__('Archive', 'king-addons'),
286 'desc' => esc_html__('Blog, categories, tags & taxonomies', 'king-addons'),
287 'default_location' => 'archive_blog',
288 ],
289 'author' => [
290 'label' => esc_html__('Author', 'king-addons'),
291 'desc' => esc_html__('Author pages', 'king-addons'),
292 'default_location' => 'author_all',
293 ],
294 'search' => [
295 'label' => esc_html__('Search', 'king-addons'),
296 'desc' => esc_html__('Search results page', 'king-addons'),
297 'default_location' => 'search_results',
298 ],
299 'not_found' => [
300 'label' => esc_html__('404', 'king-addons'),
301 'desc' => esc_html__('Not found page', 'king-addons'),
302 'default_location' => 'not_found',
303 ],
304 ];
305
306 $this->render_modern_styles();
307
308 ka_render_dark_theme_styles();
309 ka_render_dark_theme_init();
310 ?>
311 <script>
312 if (document.body) {
313 document.body.classList.add('ka-admin-v3');
314 } else {
315 document.addEventListener('DOMContentLoaded', function() {
316 document.body.classList.add('ka-admin-v3');
317 });
318 }
319 </script>
320
321 <div class="ka-tb">
322 <header class="ka-tb-header">
323 <div class="ka-tb-header-content">
324 <span class="ka-tb-title-icon" aria-hidden="true">
325 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
326 <path d="M4 4h16v16H4z" />
327 <path d="M8 8h8M8 12h8M8 16h5" />
328 </svg>
329 </span>
330 <div class="ka-tb-header-titles">
331 <h1><span class="ka-tb-title-text"><?php esc_html_e('Theme Builder', 'king-addons'); ?></span></h1>
332 <p><?php esc_html_e('Create custom templates for your site', 'king-addons'); ?></p>
333 </div>
334 </div>
335 <div class="ka-tb-header-actions">
336 <button type="button" id="ka-theme-builder-add-new" class="ka-tb-btn ka-tb-btn-primary">
337 <span class="ka-tb-btn-icon" aria-hidden="true">+</span>
338 <?php esc_html_e('Add New Template', 'king-addons'); ?>
339 </button>
340 <?php ka_render_dark_theme_toggle(); ?>
341 </div>
342 </header>
343
344 <div class="ka-tb-types" role="list">
345 <?php foreach ($type_cards as $type_slug => $data) : ?>
346 <?php $type_icon_svg = $this->get_theme_builder_type_icon_svg((string) $type_slug); ?>
347 <button
348 type="button"
349 class="ka-tb-type"
350 role="listitem"
351 data-ka-tb-type="<?php echo esc_attr($type_slug); ?>"
352 data-ka-tb-location="<?php echo esc_attr($data['default_location']); ?>"
353 >
354 <div class="ka-tb-type-icon" aria-hidden="true">
355 <?php echo $type_icon_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
356 </div>
357 <div class="ka-tb-type-label"><?php echo esc_html($data['label']); ?></div>
358 <div class="ka-tb-type-desc"><?php echo esc_html($data['desc']); ?></div>
359 </button>
360 <?php endforeach; ?>
361 </div>
362
363 <section class="ka-tb-section">
364 <div class="ka-tb-section-header">
365 <h2 class="ka-tb-section-title"><?php esc_html_e('Templates', 'king-addons'); ?></h2>
366 <div class="ka-tb-section-actions">
367 <div class="ka-tb-filters" role="navigation" aria-label="<?php echo esc_attr(esc_html__('Template filters', 'king-addons')); ?>">
368 <?php
369 $base = remove_query_arg(['status', 'paged'], $base_url);
370 $filters = [
371 'all' => esc_html__('All', 'king-addons'),
372 'enabled' => esc_html__('Enabled', 'king-addons'),
373 'disabled' => esc_html__('Disabled', 'king-addons'),
374 ];
375 foreach ($filters as $key => $label) {
376 $url = ('all' === $key) ? $base : add_query_arg('status', $key, $base);
377 $is_active = $status_filter === $key;
378 echo '<a class="ka-tb-filter' . ($is_active ? ' is-active' : '') . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
379 }
380 ?>
381 </div>
382 <div class="ka-tb-section-count"><?php echo esc_html(count($filtered_templates) . ' ' . _n('item', 'items', count($filtered_templates), 'king-addons')); ?></div>
383 </div>
384 </div>
385
386 <div class="ka-tb-templates" role="list">
387 <?php if (empty($filtered_templates)) : ?>
388 <div class="ka-tb-empty">
389 <h3 class="ka-tb-empty-title"><?php esc_html_e('No templates yet', 'king-addons'); ?></h3>
390 <p class="ka-tb-empty-desc"><?php esc_html_e('Create your first template to start customizing your site.', 'king-addons'); ?></p>
391 <button type="button" class="ka-tb-btn ka-tb-btn-primary" id="ka-theme-builder-add-new-empty">
392 <span class="ka-tb-btn-icon" aria-hidden="true">+</span>
393 <?php esc_html_e('Add New Template', 'king-addons'); ?>
394 </button>
395 </div>
396 <?php else : ?>
397 <?php foreach ($filtered_templates as $template) : ?>
398 <?php
399 $template_id = (int) ($template['id'] ?? 0);
400 if (!$template_id) {
401 continue;
402 }
403
404 $title = !empty($template['title']) ? (string) $template['title'] : sprintf(
405 /* translators: %d: template ID */
406 esc_html__('Template #%d', 'king-addons'),
407 $template_id
408 );
409
410 $edit_url = admin_url('post.php?post=' . $template_id . '&action=elementor');
411 $enabled = !empty($template['enabled']);
412 $toggle_action = $enabled ? 'disable_template' : 'enable_template';
413 $toggle_url = wp_nonce_url(add_query_arg(['action' => $toggle_action, 'template_id' => $template_id], $base_url), 'ka_theme_builder_toggle_' . $template_id);
414 $delete_url = wp_nonce_url(add_query_arg(['action' => 'delete_template', 'template_id' => $template_id], $base_url), 'ka_theme_builder_delete_' . $template_id);
415 $conditions_text = isset($template['conditions']) && is_array($template['conditions'])
416 ? $this->summarize_admin_conditions($template['conditions'])
417 : esc_html__('All', 'king-addons');
418 $type_label = !empty($template['type']) ? (string) $template['type'] : '';
419 $location_label = !empty($template['sub_location']) ? (string) $template['sub_location'] : '';
420 $priority = isset($template['priority']) ? (int) $template['priority'] : 10;
421 $title_icon_svg = $this->get_theme_builder_type_icon_svg((string) $type_label);
422 ?>
423 <div class="ka-tb-template" role="listitem">
424 <div class="ka-tb-template-status <?php echo $enabled ? 'is-enabled' : 'is-disabled'; ?>">
425 <?php echo $enabled ? esc_html__('Enabled', 'king-addons') : esc_html__('Disabled', 'king-addons'); ?>
426 </div>
427 <div class="ka-tb-template-info">
428 <a class="ka-tb-template-title" href="<?php echo esc_url($edit_url); ?>">
429 <span class="ka-tb-template-title-icon" aria-hidden="true">
430 <?php echo $title_icon_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
431 </span>
432 <span class="ka-tb-template-title-text"><?php echo esc_html($title); ?></span>
433 </a>
434 <div class="ka-tb-template-meta">
435 <?php if ($type_label) : ?>
436 <span class="ka-tb-template-type"><?php echo esc_html($type_label); ?></span>
437 <?php endif; ?>
438 <?php if ($location_label) : ?>
439 <span><?php echo esc_html($location_label); ?></span>
440 <?php endif; ?>
441 <span><?php echo esc_html(sprintf(esc_html__('Priority %d', 'king-addons'), $priority)); ?></span>
442 </div>
443 </div>
444 <button type="button" class="ka-tb-template-condition" title="<?php echo esc_attr($conditions_text); ?>">
445 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
446 <path d="M4 6h16M4 12h16M4 18h10" />
447 </svg>
448 <?php echo esc_html($conditions_text); ?>
449 </button>
450 <div class="ka-tb-template-actions">
451 <a class="ka-tb-btn ka-tb-btn-secondary" href="<?php echo esc_url($edit_url); ?>"><?php esc_html_e('Edit', 'king-addons'); ?></a>
452 <button type="button" class="ka-tb-btn ka-tb-btn-secondary ka-tb-quick-edit" data-template-id="<?php echo esc_attr((string) $template_id); ?>" data-priority="<?php echo esc_attr((string) $priority); ?>" data-enabled="<?php echo $enabled ? '1' : '0'; ?>"><?php esc_html_e('Quick Edit', 'king-addons'); ?></button>
453 <div class="ka-tb-dropdown" data-ka-dropdown>
454 <button type="button" class="ka-tb-dropdown-trigger" aria-label="<?php echo esc_attr(esc_html__('More actions', 'king-addons')); ?>">
455 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
456 <circle cx="12" cy="5" r="1" />
457 <circle cx="12" cy="12" r="1" />
458 <circle cx="12" cy="19" r="1" />
459 </svg>
460 </button>
461 <div class="ka-tb-dropdown-menu" role="menu">
462 <a class="ka-tb-dropdown-item" role="menuitem" href="<?php echo esc_url($toggle_url); ?>">
463 <?php echo $enabled ? esc_html__('Disable', 'king-addons') : esc_html__('Enable', 'king-addons'); ?>
464 </a>
465 <div class="ka-tb-dropdown-divider" aria-hidden="true"></div>
466 <a class="ka-tb-dropdown-item is-danger" role="menuitem" href="<?php echo esc_url($delete_url); ?>" onclick="return confirm('<?php echo esc_js(esc_html__('Move template to trash?', 'king-addons')); ?>');">
467 <?php esc_html_e('Delete', 'king-addons'); ?>
468 </a>
469 </div>
470 </div>
471 </div>
472 </div>
473 <?php endforeach; ?>
474 <?php endif; ?>
475 </div>
476 </section>
477
478 <?php $this->render_add_new_modal(); ?>
479 <?php $this->render_quick_edit_modal(); ?>
480 </div>
481 <?php ka_render_dark_theme_script(); ?>
482 <?php
483 }
484
485 /**
486 * Summarize conditions to a short label for admin list.
487 *
488 * @param array<string,mixed> $conditions Conditions payload.
489 *
490 * @return string
491 */
492 private function summarize_admin_conditions(array $conditions): string
493 {
494 $groups = $conditions['groups'] ?? [];
495 if (empty($groups)) {
496 return (string) esc_html__('All', 'king-addons');
497 }
498
499 $rules_count = 0;
500 foreach ($groups as $group) {
501 $rules = $group['rules'] ?? [];
502 $rules_count += is_array($rules) ? count($rules) : 0;
503 }
504
505 return (string) sprintf(
506 /* translators: %d: number of rules */
507 esc_html__('%d rules', 'king-addons'),
508 $rules_count
509 );
510 }
511
512 /**
513 * SVG icon for Theme Builder type.
514 *
515 * @param string $type_slug Theme Builder type slug.
516 *
517 * @return string
518 */
519 private function get_theme_builder_type_icon_svg(string $type_slug): string
520 {
521 $type_slug = sanitize_key($type_slug);
522 switch ($type_slug) {
523 case 'single':
524 return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h6"/></svg>';
525 case 'archive':
526 return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="6" rx="2"/><path d="M7 10v10h10V10"/><path d="M10 14h4"/></svg>';
527 case 'author':
528 return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M20 21a8 8 0 1 0-16 0"/><circle cx="12" cy="7" r="4"/></svg>';
529 case 'search':
530 return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>';
531 case 'not_found':
532 return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>';
533 default:
534 return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 4h16v16H4z"/><path d="M8 8h8M8 12h8M8 16h5"/></svg>';
535 }
536 }
537
538 /**
539 * Render modern CSS styles for Theme Builder admin page.
540 *
541 * @return void
542 */
543 private function render_modern_styles(): void
544 {
545 ?>
546 <style>
547 /* ================================================
548 Theme Builder - Match Woo Builder Admin Design
549 ================================================ */
550
551 :root {
552 --ka-tb-font: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, sans-serif;
553 --ka-tb-bg: #f5f5f7;
554 --ka-tb-surface: #ffffff;
555 --ka-tb-text: #1d1d1f;
556 --ka-tb-text-secondary: #86868b;
557 --ka-tb-border: rgba(0, 0, 0, 0.06);
558 --ka-tb-accent: #0071e3;
559 --ka-tb-accent-hover: #0077ed;
560 --ka-tb-radius: 20px;
561 --ka-tb-radius-sm: 12px;
562 --ka-tb-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
563 --ka-tb-shadow-hover: 0 8px 32px rgba(0, 0, 0, 0.10);
564 --ka-tb-transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
565 }
566
567 body.ka-v3-dark {
568 --ka-tb-bg: #000000;
569 --ka-tb-surface: #1c1c1e;
570 --ka-tb-text: #f5f5f7;
571 --ka-tb-text-secondary: #98989d;
572 --ka-tb-border: rgba(255, 255, 255, 0.10);
573 --ka-tb-shadow: 0 4px 24px rgba(0, 0, 0, 0.30);
574 --ka-tb-shadow-hover: 0 12px 40px rgba(0, 0, 0, 0.40);
575 }
576
577 body.wp-admin #wpcontent,
578 body.wp-admin #wpbody,
579 body.wp-admin #wpbody-content {
580 background: var(--ka-tb-bg) !important;
581 padding: 0 !important;
582 }
583
584 .ka-tb {
585 font-family: var(--ka-tb-font);
586 max-width: 1100px;
587 margin: 0 auto;
588 padding: 48px 40px 80px;
589 color: var(--ka-tb-text);
590 -webkit-font-smoothing: antialiased;
591 }
592 .ka-tb * { box-sizing: border-box; }
593
594 /* Header */
595 .ka-tb-header {
596 display: flex;
597 justify-content: space-between;
598 align-items: flex-start;
599 margin-bottom: 40px;
600 gap: 20px;
601 }
602 .ka-tb-header-content {
603 display: flex;
604 align-items: flex-start;
605 gap: 16px;
606 }
607
608 .ka-tb-header-titles {
609 display: flex;
610 flex-direction: column;
611 gap: 8px;
612 min-width: 0;
613 }
614
615 .ka-tb-header-titles h1 {
616 font-size: 56px;
617 font-weight: 700;
618 letter-spacing: -0.025em;
619 margin: 0;
620 line-height: 1;
621 }
622 .ka-tb-header-titles p {
623 font-size: 21px;
624 color: var(--ka-tb-text-secondary);
625 margin: 0;
626 font-weight: 400;
627 }
628 .ka-tb-title-icon {
629 width: 76px;
630 height: 76px;
631 display: inline-flex;
632 align-items: center;
633 justify-content: center;
634 border-radius: 22px;
635 background: rgba(0, 113, 227, 0.12);
636 color: var(--ka-tb-accent);
637 flex: 0 0 auto;
638 }
639 body.ka-v3-dark .ka-tb-title-icon {
640 background: rgba(10, 132, 255, 0.18);
641 color: #0a84ff;
642 }
643 .ka-tb-title-icon svg { width: 36px; height: 36px; }
644 .ka-tb-title-text {
645 background: linear-gradient(135deg, var(--ka-tb-text) 0%, var(--ka-tb-text-secondary) 100%);
646 -webkit-background-clip: text;
647 -webkit-text-fill-color: transparent;
648 background-clip: text;
649 }
650 /* subtitle moved under .ka-tb-header-titles */
651 .ka-tb-header-actions {
652 display: flex;
653 align-items: center;
654 gap: 12px;
655 flex-wrap: wrap;
656 justify-content: flex-end;
657 }
658
659 /* Buttons */
660 .ka-tb-btn {
661 display: inline-flex;
662 align-items: center;
663 justify-content: center;
664 gap: 8px;
665 padding: 10px 18px;
666 font-size: 14px;
667 font-weight: 500;
668 text-decoration: none;
669 border-radius: 980px;
670 border: none;
671 cursor: pointer;
672 transition: var(--ka-tb-transition);
673 white-space: nowrap;
674 font-family: inherit;
675 }
676 .ka-tb-btn-icon {
677 font-size: 16px;
678 line-height: 1;
679 }
680 .ka-tb-btn-primary {
681 background: var(--ka-tb-accent);
682 color: #fff;
683 }
684 .ka-tb-btn-primary:hover {
685 background: var(--ka-tb-accent-hover);
686 color: #fff;
687 transform: scale(1.02);
688 }
689 .ka-tb-btn-secondary {
690 background: rgba(0, 0, 0, 0.04);
691 color: var(--ka-tb-text);
692 }
693 body.ka-v3-dark .ka-tb-btn-secondary {
694 background: rgba(255, 255, 255, 0.08);
695 }
696 .ka-tb-btn-secondary:hover {
697 background: rgba(0, 0, 0, 0.08);
698 color: var(--ka-tb-text);
699 }
700 body.ka-v3-dark .ka-tb-btn-secondary:hover {
701 background: rgba(255, 255, 255, 0.12);
702 }
703
704 /* Template Types */
705 .ka-tb-types {
706 display: grid;
707 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
708 gap: 16px;
709 margin-bottom: 44px;
710 }
711 .ka-tb-type {
712 position: relative;
713 display: flex;
714 flex-direction: column;
715 align-items: center;
716 padding: 32px 20px;
717 background: var(--ka-tb-surface);
718 border: 1px solid var(--ka-tb-border);
719 border-radius: var(--ka-tb-radius);
720 color: var(--ka-tb-text);
721 transition: var(--ka-tb-transition);
722 cursor: pointer;
723 overflow: hidden;
724 }
725 .ka-tb-type:hover {
726 transform: translateY(-4px);
727 box-shadow: var(--ka-tb-shadow-hover);
728 border-color: var(--ka-tb-accent);
729 }
730 .ka-tb-type-icon {
731 width: 48px;
732 height: 48px;
733 margin-bottom: 16px;
734 color: var(--ka-tb-text-secondary);
735 transition: var(--ka-tb-transition);
736 }
737 .ka-tb-type:hover .ka-tb-type-icon {
738 transform: scale(1.1);
739 color: var(--ka-tb-accent);
740 }
741 .ka-tb-type-icon svg { width: 100%; height: 100%; }
742 .ka-tb-type-label {
743 font-size: 17px;
744 font-weight: 600;
745 margin-bottom: 4px;
746 text-align: center;
747 }
748 .ka-tb-type-desc {
749 font-size: 13px;
750 color: var(--ka-tb-text-secondary);
751 text-align: center;
752 }
753
754 /* Section */
755 .ka-tb-section { margin-bottom: 48px; }
756 .ka-tb-section-header {
757 display: flex;
758 justify-content: space-between;
759 align-items: center;
760 margin-bottom: 18px;
761 gap: 12px;
762 }
763 .ka-tb-section-title {
764 font-size: 28px;
765 font-weight: 600;
766 letter-spacing: -0.01em;
767 margin: 0;
768 }
769 .ka-tb-section-actions {
770 display: flex;
771 align-items: center;
772 gap: 12px;
773 }
774 .ka-tb-section-count {
775 font-size: 15px;
776 color: var(--ka-tb-text-secondary);
777 background: var(--ka-tb-surface);
778 padding: 6px 14px;
779 border-radius: 20px;
780 border: 1px solid var(--ka-tb-border);
781 white-space: nowrap;
782 }
783
784 /* Filters */
785 .ka-tb-filters {
786 display: inline-flex;
787 align-items: center;
788 gap: 6px;
789 padding: 6px;
790 border-radius: 980px;
791 border: 1px solid var(--ka-tb-border);
792 background: var(--ka-tb-surface);
793 }
794 .ka-tb-filter {
795 display: inline-flex;
796 align-items: center;
797 justify-content: center;
798 padding: 8px 12px;
799 border-radius: 980px;
800 text-decoration: none;
801 font-size: 13px;
802 font-weight: 600;
803 color: var(--ka-tb-text-secondary);
804 transition: var(--ka-tb-transition);
805 }
806 .ka-tb-filter.is-active {
807 background: rgba(0, 113, 227, 0.12);
808 color: var(--ka-tb-accent);
809 }
810 .ka-tb-filter:hover {
811 background: rgba(0, 0, 0, 0.04);
812 color: var(--ka-tb-text);
813 }
814 body.ka-v3-dark .ka-tb-filter:hover {
815 background: rgba(255, 255, 255, 0.06);
816 }
817
818 /* Templates List */
819 .ka-tb-templates {
820 background: var(--ka-tb-surface);
821 border-radius: var(--ka-tb-radius);
822 border: 1px solid var(--ka-tb-border);
823 overflow: visible;
824 }
825 .ka-tb-template {
826 display: grid;
827 grid-template-columns: auto 1fr auto auto;
828 align-items: center;
829 gap: 20px;
830 padding: 20px 24px;
831 border-bottom: 1px solid var(--ka-tb-border);
832 transition: var(--ka-tb-transition);
833 }
834 .ka-tb-template:last-child { border-bottom: none; }
835 .ka-tb-template:hover { background: rgba(0, 113, 227, 0.03); }
836 body.ka-v3-dark .ka-tb-template:hover { background: rgba(255, 255, 255, 0.03); }
837
838 .ka-tb-template-info {
839 display: flex;
840 flex-direction: column;
841 gap: 4px;
842 min-width: 0;
843 }
844 .ka-tb-template-title {
845 font-size: 16px;
846 font-weight: 500;
847 color: var(--ka-tb-text);
848 text-decoration: none;
849 display: inline-flex;
850 align-items: center;
851 gap: 8px;
852 min-width: 0;
853 transition: var(--ka-tb-transition);
854 }
855 .ka-tb-template-title-icon {
856 width: 18px;
857 height: 18px;
858 display: inline-flex;
859 align-items: center;
860 justify-content: center;
861 color: var(--ka-tb-text-secondary);
862 flex: 0 0 auto;
863 }
864 .ka-tb-template-title-icon svg { width: 18px; height: 18px; }
865 .ka-tb-template-title-text {
866 min-width: 0;
867 white-space: nowrap;
868 overflow: hidden;
869 text-overflow: ellipsis;
870 }
871 .ka-tb-template-title:hover { color: var(--ka-tb-accent); }
872 .ka-tb-template-meta {
873 display: flex;
874 align-items: center;
875 gap: 12px;
876 font-size: 13px;
877 color: var(--ka-tb-text-secondary);
878 flex-wrap: wrap;
879 }
880 .ka-tb-template-type {
881 display: inline-flex;
882 align-items: center;
883 gap: 6px;
884 }
885 .ka-tb-template-type::before {
886 content: '';
887 width: 6px;
888 height: 6px;
889 background: var(--ka-tb-accent);
890 border-radius: 50%;
891 }
892
893 .ka-tb-template-status {
894 display: inline-flex;
895 align-items: center;
896 padding: 6px 14px;
897 font-size: 13px;
898 font-weight: 600;
899 border-radius: 8px;
900 white-space: nowrap;
901 min-width: 90px;
902 justify-content: center;
903 justify-self: start;
904 }
905 .ka-tb-template-status.is-enabled {
906 background: rgba(52, 199, 89, 0.15);
907 color: #30d158;
908 }
909 .ka-tb-template-status.is-disabled {
910 background: rgba(255, 149, 0, 0.15);
911 color: #ff9f0a;
912 }
913
914 .ka-tb-template-condition {
915 display: inline-flex;
916 align-items: center;
917 gap: 8px;
918 padding: 10px 18px;
919 min-height: 40px;
920 font-size: 14px;
921 font-weight: 500;
922 border-radius: 980px;
923 background: rgba(0, 113, 227, 0.10);
924 color: var(--ka-tb-accent);
925 max-width: 240px;
926 overflow: hidden;
927 text-overflow: ellipsis;
928 white-space: nowrap;
929 border: none;
930 cursor: default;
931 }
932 .ka-tb-template-condition svg {
933 width: 18px;
934 height: 18px;
935 flex-shrink: 0;
936 opacity: 0.7;
937 }
938
939 .ka-tb-template-actions {
940 display: flex;
941 align-items: center;
942 gap: 12px;
943 flex-wrap: nowrap;
944 }
945
946 /* Dropdown */
947 .ka-tb-dropdown { position: relative; }
948 .ka-tb-dropdown-trigger {
949 display: flex;
950 align-items: center;
951 justify-content: center;
952 width: 40px;
953 height: 40px;
954 border-radius: 10px;
955 background: transparent;
956 border: 1px solid transparent;
957 cursor: pointer;
958 transition: var(--ka-tb-transition);
959 color: var(--ka-tb-text-secondary);
960 }
961 .ka-tb-dropdown-trigger:hover {
962 background: var(--ka-tb-surface);
963 border-color: var(--ka-tb-border);
964 color: var(--ka-tb-text);
965 }
966 .ka-tb-dropdown-trigger svg { width: 20px; height: 20px; }
967 .ka-tb-dropdown-menu {
968 position: absolute;
969 top: 100%;
970 right: 0;
971 z-index: 100;
972 min-width: 180px;
973 padding: 8px 0;
974 background: var(--ka-tb-surface);
975 border: 1px solid var(--ka-tb-border);
976 border-radius: var(--ka-tb-radius-sm);
977 box-shadow: var(--ka-tb-shadow-hover);
978 opacity: 0;
979 visibility: hidden;
980 transform: translateY(-8px);
981 transition: var(--ka-tb-transition);
982 }
983 .ka-tb-dropdown.is-open .ka-tb-dropdown-menu {
984 opacity: 1;
985 visibility: visible;
986 transform: translateY(4px);
987 }
988 .ka-tb-dropdown-item {
989 display: flex;
990 align-items: center;
991 gap: 10px;
992 padding: 10px 16px;
993 font-size: 14px;
994 color: var(--ka-tb-text);
995 text-decoration: none;
996 cursor: pointer;
997 transition: var(--ka-tb-transition);
998 }
999 .ka-tb-dropdown-item:hover { background: var(--ka-tb-border); }
1000 .ka-tb-dropdown-item.is-danger { color: #ff3b30; }
1001 .ka-tb-dropdown-divider { height: 1px; margin: 8px 0; background: var(--ka-tb-border); }
1002
1003 /* Empty state */
1004 .ka-tb-empty {
1005 padding: 42px 24px;
1006 text-align: center;
1007 }
1008 .ka-tb-empty-title {
1009 margin: 0 0 8px;
1010 font-size: 20px;
1011 font-weight: 600;
1012 }
1013 .ka-tb-empty-desc {
1014 margin: 0 0 18px;
1015 color: var(--ka-tb-text-secondary);
1016 font-size: 15px;
1017 }
1018
1019 /* Modal - Premium Design */
1020 .ka-theme-builder-modal { display: none; }
1021 .ka-tb-modal-overlay {
1022 position: fixed;
1023 top: 0;
1024 left: 0;
1025 right: 0;
1026 bottom: 0;
1027 z-index: 100000;
1028 background: rgba(0, 0, 0, 0.55);
1029 backdrop-filter: blur(12px);
1030 -webkit-backdrop-filter: blur(12px);
1031 display: flex;
1032 align-items: center;
1033 justify-content: center;
1034 opacity: 0;
1035 visibility: hidden;
1036 transition: opacity 0.25s ease, visibility 0.25s ease;
1037 padding: 20px;
1038 }
1039 .ka-tb-modal-overlay.is-open {
1040 opacity: 1;
1041 visibility: visible;
1042 }
1043 .ka-tb-modal {
1044 width: 100%;
1045 max-width: 480px;
1046 padding: 40px 44px 36px;
1047 background: var(--ka-tb-surface);
1048 border-radius: 24px;
1049 box-shadow: 0 32px 100px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
1050 transform: scale(0.92) translateY(20px);
1051 transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.25s ease;
1052 opacity: 0;
1053 }
1054 body.ka-v3-dark .ka-tb-modal {
1055 background: rgba(28, 28, 30, 0.92);
1056 border: 1px solid rgba(255, 255, 255, 0.12);
1057 box-shadow: 0 32px 100px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.06) inset;
1058 }
1059 .ka-tb-modal-overlay.is-open .ka-tb-modal {
1060 transform: scale(1) translateY(0);
1061 opacity: 1;
1062 }
1063 .ka-tb-modal h3 {
1064 font-size: 28px;
1065 font-weight: 700;
1066 margin: 0 0 6px;
1067 color: var(--ka-tb-text);
1068 letter-spacing: -0.03em;
1069 line-height: 1.15;
1070 }
1071 .ka-tb-modal-desc {
1072 font-size: 16px;
1073 color: var(--ka-tb-text-secondary);
1074 margin: 0 0 28px;
1075 line-height: 1.5;
1076 }
1077 .ka-tb-form-group {
1078 margin-bottom: 20px;
1079 }
1080 .ka-tb-form-label {
1081 display: block;
1082 font-size: 12px;
1083 font-weight: 600;
1084 color: var(--ka-tb-text-secondary);
1085 margin-bottom: 8px;
1086 text-transform: uppercase;
1087 letter-spacing: 0.6px;
1088 }
1089 .ka-tb-modal input[type="text"].ka-tb-modal-input,
1090 .ka-tb-modal-input {
1091 width: 100% !important;
1092 padding: 16px 20px !important;
1093 font-size: 17px !important;
1094 font-family: inherit !important;
1095 font-weight: 400 !important;
1096 border: 2px solid rgba(84, 84, 88, 0.36) !important;
1097 border-radius: 14px !important;
1098 background: #2c2c2e !important;
1099 color: #f5f5f7 !important;
1100 transition: all 0.2s ease !important;
1101 box-shadow: none !important;
1102 -webkit-appearance: none !important;
1103 appearance: none !important;
1104 line-height: 1.4 !important;
1105 height: auto !important;
1106 margin: 0 !important;
1107 }
1108 body:not(.ka-v3-dark) .ka-tb-modal input[type="text"].ka-tb-modal-input,
1109 body:not(.ka-v3-dark) .ka-tb-modal-input {
1110 background: #f5f5f7 !important;
1111 border-color: rgba(0, 0, 0, 0.12) !important;
1112 color: #1d1d1f !important;
1113 }
1114 .ka-tb-modal-input::placeholder {
1115 color: #98989d !important;
1116 opacity: 1 !important;
1117 }
1118 .ka-tb-modal-input:hover {
1119 border-color: rgba(10, 132, 255, 0.5) !important;
1120 }
1121 body:not(.ka-v3-dark) .ka-tb-modal-input:hover {
1122 border-color: rgba(0, 113, 227, 0.5) !important;
1123 }
1124 .ka-tb-modal-input:focus {
1125 outline: none !important;
1126 border-color: #0a84ff !important;
1127 background: #3a3a3c !important;
1128 box-shadow: 0 0 0 4px rgba(10, 132, 255, 0.25) !important;
1129 }
1130 body:not(.ka-v3-dark) .ka-tb-modal-input:focus {
1131 background: #ffffff !important;
1132 border-color: #0071e3 !important;
1133 box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.15) !important;
1134 }
1135 .ka-tb-modal select.ka-tb-form-select,
1136 .ka-tb-form-select {
1137 width: 100% !important;
1138 padding: 16px 52px 16px 20px !important;
1139 font-size: 17px !important;
1140 font-family: inherit !important;
1141 font-weight: 400 !important;
1142 border: 2px solid rgba(84, 84, 88, 0.36) !important;
1143 border-radius: 14px !important;
1144 background-color: #2c2c2e !important;
1145 color: #f5f5f7 !important;
1146 transition: all 0.2s ease !important;
1147 cursor: pointer !important;
1148 appearance: none !important;
1149 -webkit-appearance: none !important;
1150 -moz-appearance: none !important;
1151 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f7' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") !important;
1152 background-repeat: no-repeat !important;
1153 background-position: right 18px center !important;
1154 background-size: 20px 20px !important;
1155 line-height: 1.4 !important;
1156 height: auto !important;
1157 margin: 0 !important;
1158 box-shadow: none !important;
1159 }
1160 body:not(.ka-v3-dark) .ka-tb-modal select.ka-tb-form-select,
1161 body:not(.ka-v3-dark) .ka-tb-form-select {
1162 background-color: #f5f5f7 !important;
1163 border-color: rgba(0, 0, 0, 0.12) !important;
1164 color: #1d1d1f !important;
1165 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%231d1d1f' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") !important;
1166 }
1167 .ka-tb-form-select:hover {
1168 border-color: rgba(10, 132, 255, 0.5) !important;
1169 }
1170 body:not(.ka-v3-dark) .ka-tb-form-select:hover {
1171 border-color: rgba(0, 113, 227, 0.5) !important;
1172 }
1173 .ka-tb-form-select:focus {
1174 outline: none !important;
1175 border-color: #0a84ff !important;
1176 background-color: #3a3a3c !important;
1177 box-shadow: 0 0 0 4px rgba(10, 132, 255, 0.25) !important;
1178 }
1179 body:not(.ka-v3-dark) .ka-tb-form-select:focus {
1180 background-color: #ffffff !important;
1181 border-color: #0071e3 !important;
1182 box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.15) !important;
1183 }
1184 .ka-tb-form-select option {
1185 background: #2c2c2e !important;
1186 color: #f5f5f7 !important;
1187 padding: 12px !important;
1188 }
1189 body:not(.ka-v3-dark) .ka-tb-form-select option {
1190 background: #ffffff !important;
1191 color: #1d1d1f !important;
1192 }
1193 .ka-tb-modal-actions {
1194 display: flex;
1195 gap: 12px;
1196 margin-top: 28px;
1197 justify-content: flex-end;
1198 flex-wrap: wrap;
1199 }
1200 .ka-tb-modal-actions .ka-tb-btn {
1201 padding: 14px 24px;
1202 font-size: 15px;
1203 font-weight: 500;
1204 }
1205 .ka-tb-modal-actions .ka-tb-btn-primary {
1206 min-width: 180px;
1207 }
1208
1209 /* Responsive */
1210 @media (max-width: 782px) {
1211 .ka-tb { padding: 36px 20px 70px; }
1212 .ka-tb-header { flex-direction: column; align-items: flex-start; }
1213 .ka-tb-header-titles h1 { font-size: 44px; }
1214 .ka-tb-title-icon { width: 64px; height: 64px; border-radius: 18px; }
1215 .ka-tb-title-icon svg { width: 30px; height: 30px; }
1216 .ka-tb-template { grid-template-columns: 1fr; gap: 12px; }
1217 .ka-tb-template-actions { justify-content: flex-start; flex-wrap: wrap; }
1218 .ka-tb-section-header { flex-direction: column; align-items: stretch; }
1219 .ka-tb-section-actions { flex-wrap: wrap; width: 100%; }
1220 .ka-tb-filters { flex-wrap: wrap; border-radius: 16px; max-width: 100%; }
1221 }
1222 /* (Old table/modal styles removed; Theme Builder now mirrors Woo Builder UI) */
1223 </style>
1224 <?php
1225 }
1226
1227 /**
1228 * Enqueue front-end assets placeholder.
1229 *
1230 * @return void
1231 */
1232 public function enqueue_front_assets(): void
1233 {
1234 // Reserved for future front-end assets; keep file reference valid.
1235 }
1236
1237 /**
1238 * Enqueue preview handler for Elementor editor.
1239 *
1240 * @return void
1241 */
1242 public function enqueue_preview_handler(): void
1243 {
1244 $post_id = get_the_ID() ? (int) get_the_ID() : get_queried_object_id();
1245
1246 if (!$post_id || 'elementor_library' !== get_post_type($post_id)) {
1247 return;
1248 }
1249
1250 if (!get_post_meta($post_id, Meta_Keys::LOCATION, true)) {
1251 return;
1252 }
1253
1254 wp_enqueue_script(
1255 KING_ADDONS_ASSETS_UNIQUE_KEY . '-theme-builder-preview',
1256 KING_ADDONS_URL . 'includes/extensions/Theme_Builder/preview-handler.js',
1257 ['jquery'],
1258 KING_ADDONS_VERSION,
1259 true
1260 );
1261 }
1262
1263 /**
1264 * Register Elementor document controls for Theme Builder templates.
1265 *
1266 * @param mixed $document Document instance.
1267 *
1268 * @return void
1269 */
1270 public function register_document_controls($document): void
1271 {
1272 if (!is_object($document) || !method_exists($document, 'get_main_id')) {
1273 return;
1274 }
1275
1276 $post_id = (int) $document->get_main_id();
1277 if ('elementor_library' !== get_post_type($post_id)) {
1278 return;
1279 }
1280 $location = get_post_meta($post_id, Meta_Keys::LOCATION, true);
1281
1282 if (!$location) {
1283 return;
1284 }
1285
1286 $sub_location = get_post_meta($post_id, Meta_Keys::SUB_LOCATION, true);
1287 $preview_post = (int) get_post_meta($post_id, Meta_Keys::PREVIEW_POST_ID, true);
1288 $preview_term = (int) get_post_meta($post_id, Meta_Keys::PREVIEW_TERM_ID, true);
1289 $preview_author = (int) get_post_meta($post_id, Meta_Keys::PREVIEW_AUTHOR_ID, true);
1290 $preview_query = get_post_meta($post_id, Meta_Keys::PREVIEW_QUERY, true);
1291
1292 $document->start_controls_section(
1293 'ka_theme_builder_settings',
1294 [
1295 'label' => esc_html__('Theme Builder', 'king-addons'),
1296 'tab' => Controls_Manager::TAB_SETTINGS,
1297 ]
1298 );
1299
1300 $document->add_control(
1301 'ka_tb_readonly_type',
1302 [
1303 'label' => esc_html__('Template Type', 'king-addons'),
1304 'type' => Controls_Manager::RAW_HTML,
1305 'raw' => '<strong>' . esc_html($location) . '</strong>',
1306 'content_classes' => 'ka-theme-builder-readonly',
1307 ]
1308 );
1309
1310 $document->add_control(
1311 'ka_tb_readonly_location',
1312 [
1313 'label' => esc_html__('Location', 'king-addons'),
1314 'type' => Controls_Manager::RAW_HTML,
1315 'raw' => '<strong>' . esc_html($sub_location) . '</strong>',
1316 'content_classes' => 'ka-theme-builder-readonly',
1317 ]
1318 );
1319
1320 $document->add_control(
1321 'ka_tb_preview_post',
1322 [
1323 'label' => esc_html__('Preview Post ID', 'king-addons'),
1324 'type' => Controls_Manager::NUMBER,
1325 'default' => $preview_post,
1326 ]
1327 );
1328
1329 $document->add_control(
1330 'ka_tb_preview_term',
1331 [
1332 'label' => esc_html__('Preview Term ID', 'king-addons'),
1333 'type' => Controls_Manager::NUMBER,
1334 'default' => $preview_term,
1335 ]
1336 );
1337
1338 $document->add_control(
1339 'ka_tb_preview_author',
1340 [
1341 'label' => esc_html__('Preview Author ID', 'king-addons'),
1342 'type' => Controls_Manager::NUMBER,
1343 'default' => $preview_author,
1344 ]
1345 );
1346
1347 $document->add_control(
1348 'ka_tb_preview_query',
1349 [
1350 'label' => esc_html__('Preview Search Query', 'king-addons'),
1351 'type' => Controls_Manager::TEXT,
1352 'default' => $preview_query,
1353 ]
1354 );
1355
1356 if ($this->is_pro_location($sub_location)) {
1357 $document->add_control(
1358 'ka_tb_pro_notice',
1359 [
1360 'label' => esc_html__('Pro', 'king-addons'),
1361 'type' => Controls_Manager::RAW_HTML,
1362 'raw' => esc_html__('Advanced conditions require Pro.', 'king-addons'),
1363 'content_classes' => 'ka-theme-builder-readonly',
1364 ]
1365 );
1366 }
1367
1368 $document->end_controls_section();
1369 }
1370
1371 /**
1372 * Persist preview meta from Elementor document settings.
1373 *
1374 * @param \Elementor\Core\Base\Document $document Elementor document.
1375 * @param array<string,mixed> $data Saved data.
1376 *
1377 * @return void
1378 */
1379 public function handle_document_after_save($document, array $data): void
1380 {
1381 if (!is_object($document) || !method_exists($document, 'get_main_id')) {
1382 return;
1383 }
1384
1385 $post_id = (int) $document->get_main_id();
1386 if ('elementor_library' !== get_post_type($post_id)) {
1387 return;
1388 }
1389
1390 if (!get_post_meta($post_id, Meta_Keys::LOCATION, true)) {
1391 return;
1392 }
1393
1394 $preview_post = (int) $document->get_settings('ka_tb_preview_post');
1395 $preview_term = (int) $document->get_settings('ka_tb_preview_term');
1396 $preview_author = (int) $document->get_settings('ka_tb_preview_author');
1397 $preview_query = sanitize_text_field((string) $document->get_settings('ka_tb_preview_query'));
1398
1399 update_post_meta($post_id, Meta_Keys::PREVIEW_POST_ID, $preview_post);
1400 update_post_meta($post_id, Meta_Keys::PREVIEW_TERM_ID, $preview_term);
1401 update_post_meta($post_id, Meta_Keys::PREVIEW_AUTHOR_ID, $preview_author);
1402 update_post_meta($post_id, Meta_Keys::PREVIEW_QUERY, $preview_query);
1403
1404 $this->repository->clear_cache();
1405 }
1406
1407 /**
1408 * Handle "Add New" submission.
1409 *
1410 * @return void
1411 */
1412 public function handle_create_template(): void
1413 {
1414 if (!current_user_can('manage_options')) {
1415 wp_die(esc_html__('You do not have permission to perform this action.', 'king-addons'));
1416 }
1417
1418 if (!isset($_POST['ka_theme_builder_create_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['ka_theme_builder_create_nonce'])), 'ka_theme_builder_create')) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
1419 wp_die(esc_html__('Invalid nonce.', 'king-addons'));
1420 }
1421
1422 $title = isset($_POST['ka_tb_title']) ? sanitize_text_field(wp_unslash($_POST['ka_tb_title'])) : esc_html__('New Theme Template', 'king-addons');
1423 $type = isset($_POST['ka_tb_type']) ? sanitize_text_field(wp_unslash($_POST['ka_tb_type'])) : 'single';
1424 $sub_location = isset($_POST['ka_tb_location']) ? sanitize_text_field(wp_unslash($_POST['ka_tb_location'])) : '';
1425 $type = $this->normalize_type($type, $sub_location);
1426
1427 // Check Free tier limit: 1 active template per primary type
1428 if (!$this->can_use_pro()) {
1429 $primary_type = $this->normalize_type($type, $sub_location);
1430 if ($this->count_enabled_templates_by_type($primary_type) >= 1) {
1431 wp_die(
1432 sprintf(
1433 /* translators: %s: template type name */
1434 esc_html__('Free version allows only 1 active %s template. Disable an existing template or upgrade to Pro for unlimited templates.', 'king-addons'),
1435 esc_html($primary_type)
1436 )
1437 );
1438 }
1439 }
1440
1441 $post_id = wp_insert_post([
1442 'post_type' => 'elementor_library',
1443 'post_status' => 'publish',
1444 'post_title' => $title,
1445 ]);
1446
1447 if (is_wp_error($post_id)) {
1448 wp_die(esc_html__('Unable to create template.', 'king-addons'));
1449 }
1450
1451 $conditions = $this->build_default_conditions($sub_location);
1452
1453 $is_pro = Conditions::is_pro_only($conditions) || $this->is_pro_location($sub_location);
1454 $enabled = $is_pro && !$this->can_use_pro() ? '0' : '1';
1455
1456 update_post_meta($post_id, Meta_Keys::ENABLED, $enabled);
1457 update_post_meta($post_id, Meta_Keys::LOCATION, $type);
1458 update_post_meta($post_id, Meta_Keys::SUB_LOCATION, $sub_location);
1459 update_post_meta($post_id, Meta_Keys::PRIORITY, 10);
1460 update_post_meta($post_id, Meta_Keys::IS_PRO_ONLY, $is_pro ? '1' : '0');
1461 update_post_meta($post_id, Meta_Keys::CONDITIONS, wp_json_encode($conditions));
1462
1463 $this->repository->clear_cache();
1464
1465 wp_safe_redirect(admin_url('post.php?post=' . $post_id . '&action=elementor'));
1466 exit;
1467 }
1468
1469 /**
1470 * Handle quick update submission.
1471 *
1472 * @return void
1473 */
1474 public function handle_quick_update(): void
1475 {
1476 if (!current_user_can('manage_options')) {
1477 wp_die(esc_html__('You do not have permission to perform this action.', 'king-addons'));
1478 }
1479
1480 if (!isset($_POST['ka_theme_builder_quick_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['ka_theme_builder_quick_nonce'])), 'ka_theme_builder_quick')) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
1481 wp_die(esc_html__('Invalid nonce.', 'king-addons'));
1482 }
1483
1484 $template_id = isset($_POST['template_id']) ? (int) $_POST['template_id'] : 0;
1485 $priority = isset($_POST['priority']) ? (int) $_POST['priority'] : 10;
1486 $enabled = isset($_POST['enabled']) && '1' === $_POST['enabled']; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1487
1488 if ($template_id > 0) {
1489 update_post_meta($template_id, Meta_Keys::PRIORITY, $priority);
1490 update_post_meta($template_id, Meta_Keys::ENABLED, $enabled ? '1' : '0');
1491 $this->repository->clear_cache();
1492 }
1493
1494 wp_safe_redirect(admin_url('admin.php?page=' . $this->menu_slug));
1495 exit;
1496 }
1497
1498 /**
1499 * Handle template duplication (Pro feature).
1500 *
1501 * @return void
1502 */
1503 public function handle_duplicate_template(): void
1504 {
1505 if (!current_user_can('manage_options')) {
1506 wp_die(esc_html__('You do not have permission to perform this action.', 'king-addons'));
1507 }
1508
1509 if (!$this->can_use_pro()) {
1510 wp_die(esc_html__('Template duplication requires Pro.', 'king-addons'));
1511 }
1512
1513 $template_id = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0;
1514
1515 if (!$template_id || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'ka_theme_builder_duplicate_' . $template_id)) {
1516 wp_die(esc_html__('Invalid request.', 'king-addons'));
1517 }
1518
1519 $original = get_post($template_id);
1520 if (!$original || 'elementor_library' !== $original->post_type) {
1521 wp_die(esc_html__('Template not found.', 'king-addons'));
1522 }
1523
1524 // Clone the post
1525 $new_post_id = wp_insert_post([
1526 'post_type' => 'elementor_library',
1527 'post_status' => 'draft',
1528 'post_title' => $original->post_title . ' ' . esc_html__('(Copy)', 'king-addons'),
1529 'post_content' => $original->post_content,
1530 ]);
1531
1532 if (is_wp_error($new_post_id)) {
1533 wp_die(esc_html__('Failed to duplicate template.', 'king-addons'));
1534 }
1535
1536 // Copy all meta data
1537 $meta_keys = [
1538 Meta_Keys::ENABLED,
1539 Meta_Keys::LOCATION,
1540 Meta_Keys::SUB_LOCATION,
1541 Meta_Keys::CONDITIONS,
1542 Meta_Keys::PRIORITY,
1543 Meta_Keys::IS_PRO_ONLY,
1544 Meta_Keys::PREVIEW_POST_ID,
1545 Meta_Keys::PREVIEW_TERM_ID,
1546 Meta_Keys::PREVIEW_AUTHOR_ID,
1547 Meta_Keys::PREVIEW_QUERY,
1548 ];
1549
1550 foreach ($meta_keys as $key) {
1551 $value = get_post_meta($template_id, $key, true);
1552 if ('' !== $value) {
1553 update_post_meta($new_post_id, $key, $value);
1554 }
1555 }
1556
1557 // Disable the duplicate by default to avoid conflicts
1558 update_post_meta($new_post_id, Meta_Keys::ENABLED, '0');
1559
1560 // Copy Elementor data
1561 $elementor_data = get_post_meta($template_id, '_elementor_data', true);
1562 if ($elementor_data) {
1563 update_post_meta($new_post_id, '_elementor_data', $elementor_data);
1564 }
1565
1566 $elementor_edit_mode = get_post_meta($template_id, '_elementor_edit_mode', true);
1567 if ($elementor_edit_mode) {
1568 update_post_meta($new_post_id, '_elementor_edit_mode', $elementor_edit_mode);
1569 }
1570
1571 $this->repository->clear_cache();
1572
1573 wp_safe_redirect(admin_url('admin.php?page=' . $this->menu_slug . '&duplicated=1'));
1574 exit;
1575 }
1576
1577 /**
1578 * Handle template include override.
1579 *
1580 * @param string $template Current template path.
1581 *
1582 * @return string
1583 */
1584 public function handle_template_include(string $template): string
1585 {
1586 if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) {
1587 return $template;
1588 }
1589
1590 if (!did_action('elementor/loaded')) {
1591 return $template;
1592 }
1593
1594 if (is_singular('elementor_library')) {
1595 return $template;
1596 }
1597
1598 if ($this->is_woo_context()) {
1599 return $template;
1600 }
1601
1602 $context = Context::from_request();
1603 $resolved = $this->resolver->resolve($context);
1604
1605 if (!$resolved) {
1606 return $template;
1607 }
1608
1609 $this->current_template_id = $resolved;
1610
1611 return KING_ADDONS_PATH . 'includes/extensions/Theme_Builder/templates/theme-builder.php';
1612 }
1613
1614 /**
1615 * Provide current template ID to filters.
1616 *
1617 * @param int $template_id Existing template id.
1618 *
1619 * @return int
1620 */
1621 public function filter_current_template_id(int $template_id): int
1622 {
1623 if ($this->current_template_id) {
1624 return (int) $this->current_template_id;
1625 }
1626
1627 return (int) $template_id;
1628 }
1629
1630 /**
1631 * Append body class for resolved template.
1632 *
1633 * @param array<int,string> $classes Body classes.
1634 *
1635 * @return array<int,string>
1636 */
1637 public function filter_body_class(array $classes): array
1638 {
1639 if ($this->current_template_id) {
1640 $classes[] = 'king-addons-theme-builder-active';
1641 }
1642
1643 return $classes;
1644 }
1645
1646 /**
1647 * Render add-new modal markup.
1648 *
1649 * @return void
1650 */
1651 private function render_add_new_modal(): void
1652 {
1653 ?>
1654 <div id="ka-theme-builder-modal" class="ka-tb-modal-overlay" aria-hidden="true">
1655 <div class="ka-tb-modal" role="dialog" aria-modal="true" aria-labelledby="ka-tb-create-title">
1656 <h3 id="ka-tb-create-title"><?php echo esc_html__('Create Theme Template', 'king-addons'); ?></h3>
1657 <p class="ka-tb-modal-desc"><?php echo esc_html__('Choose the template type and where it should apply.', 'king-addons'); ?></p>
1658
1659 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
1660 <input type="hidden" name="action" value="ka_theme_builder_create" />
1661 <?php wp_nonce_field('ka_theme_builder_create', 'ka_theme_builder_create_nonce'); ?>
1662
1663 <div class="ka-tb-form-group">
1664 <label class="ka-tb-form-label" for="ka-tb-title"><?php echo esc_html__('Template Name', 'king-addons'); ?></label>
1665 <input type="text" id="ka-tb-title" name="ka_tb_title" class="ka-tb-modal-input" value="<?php echo esc_attr__('Theme Template', 'king-addons'); ?>" />
1666 </div>
1667
1668 <div class="ka-tb-form-group">
1669 <label class="ka-tb-form-label" for="ka-tb-type"><?php echo esc_html__('Template Type', 'king-addons'); ?></label>
1670 <select id="ka-tb-type" name="ka_tb_type" class="ka-tb-form-select">
1671 <option value="single"><?php echo esc_html__('Single', 'king-addons'); ?></option>
1672 <option value="archive"><?php echo esc_html__('Archive', 'king-addons'); ?></option>
1673 <option value="author"><?php echo esc_html__('Author', 'king-addons'); ?></option>
1674 <option value="search"><?php echo esc_html__('Search', 'king-addons'); ?></option>
1675 <option value="not_found"><?php echo esc_html__('404', 'king-addons'); ?></option>
1676 </select>
1677 </div>
1678
1679 <div class="ka-tb-form-group">
1680 <label class="ka-tb-form-label" for="ka-tb-location"><?php echo esc_html__('Location', 'king-addons'); ?></label>
1681 <select id="ka-tb-location" name="ka_tb_location" class="ka-tb-form-select">
1682 <option value="single_post"><?php echo esc_html__('All Posts', 'king-addons'); ?></option>
1683 <option value="single_page"><?php echo esc_html__('All Pages', 'king-addons'); ?></option>
1684 <option value="single_cpt"><?php echo esc_html__('Custom Post Types (Pro)', 'king-addons'); ?></option>
1685 <option value="archive_blog"><?php echo esc_html__('Blog / Posts Page', 'king-addons'); ?></option>
1686 <option value="archive_category"><?php echo esc_html__('All Categories', 'king-addons'); ?></option>
1687 <option value="archive_tag"><?php echo esc_html__('All Tags', 'king-addons'); ?></option>
1688 <option value="archive_tax"><?php echo esc_html__('Custom Taxonomy (Pro)', 'king-addons'); ?></option>
1689 <option value="author_all"><?php echo esc_html__('All Authors (Pro)', 'king-addons'); ?></option>
1690 <option value="author_specific"><?php echo esc_html__('Specific Authors (Pro)', 'king-addons'); ?></option>
1691 <option value="search_results"><?php echo esc_html__('Search Results', 'king-addons'); ?></option>
1692 <option value="not_found"><?php echo esc_html__('404 Page', 'king-addons'); ?></option>
1693 </select>
1694 </div>
1695
1696 <div class="ka-tb-modal-actions">
1697 <button type="button" class="ka-tb-btn ka-tb-btn-secondary ka-tb-modal-close"><?php echo esc_html__('Cancel', 'king-addons'); ?></button>
1698 <button type="submit" class="ka-tb-btn ka-tb-btn-primary"><?php echo esc_html__('Create and Edit with Elementor', 'king-addons'); ?></button>
1699 </div>
1700 </form>
1701 </div>
1702 </div>
1703 <?php
1704 }
1705
1706 /**
1707 * Render quick edit modal markup.
1708 *
1709 * @return void
1710 */
1711 private function render_quick_edit_modal(): void
1712 {
1713 ?>
1714 <div id="ka-theme-builder-quick-modal" class="ka-tb-modal-overlay" aria-hidden="true">
1715 <div class="ka-tb-modal" role="dialog" aria-modal="true" aria-labelledby="ka-tb-quick-title">
1716 <h3 id="ka-tb-quick-title"><?php echo esc_html__('Quick Edit Template', 'king-addons'); ?></h3>
1717 <p class="ka-tb-modal-desc"><?php echo esc_html__('Change priority and enabled status.', 'king-addons'); ?></p>
1718
1719 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
1720 <input type="hidden" name="action" value="ka_theme_builder_quick_update" />
1721 <?php wp_nonce_field('ka_theme_builder_quick', 'ka_theme_builder_quick_nonce'); ?>
1722 <input type="hidden" id="ka-tb-quick-template-id" name="template_id" value="" />
1723
1724 <div class="ka-tb-form-group">
1725 <label class="ka-tb-form-label" for="ka-tb-quick-priority"><?php echo esc_html__('Priority', 'king-addons'); ?></label>
1726 <input type="number" id="ka-tb-quick-priority" name="priority" min="0" step="1" value="10" class="ka-tb-modal-input" />
1727 </div>
1728
1729 <div class="ka-tb-form-group">
1730 <label class="ka-tb-form-label" for="ka-tb-quick-enabled"><?php echo esc_html__('Status', 'king-addons'); ?></label>
1731 <select id="ka-tb-quick-enabled" name="enabled" class="ka-tb-form-select">
1732 <option value="1"><?php echo esc_html__('Enabled', 'king-addons'); ?></option>
1733 <option value="0"><?php echo esc_html__('Disabled', 'king-addons'); ?></option>
1734 </select>
1735 </div>
1736
1737 <div class="ka-tb-modal-actions">
1738 <button type="button" class="ka-tb-btn ka-tb-btn-secondary ka-tb-modal-close"><?php echo esc_html__('Cancel', 'king-addons'); ?></button>
1739 <button type="submit" class="ka-tb-btn ka-tb-btn-primary"><?php echo esc_html__('Save', 'king-addons'); ?></button>
1740 </div>
1741 </form>
1742 </div>
1743 </div>
1744 <?php
1745 }
1746
1747 /**
1748 * Build default conditions for a selected location.
1749 *
1750 * @param string $sub_location Sub-location value.
1751 *
1752 * @return array<string,mixed>
1753 */
1754 private function build_default_conditions(string $sub_location): array
1755 {
1756 $groups = [];
1757
1758 switch ($sub_location) {
1759 case 'single_post':
1760 $groups[] = [
1761 'relation' => 'AND',
1762 'rules' => [
1763 [
1764 'type' => 'include',
1765 'target' => 'post_type',
1766 'operator' => 'in',
1767 'value' => ['post'],
1768 ],
1769 ],
1770 ];
1771 break;
1772 case 'single_page':
1773 $groups[] = [
1774 'relation' => 'AND',
1775 'rules' => [
1776 [
1777 'type' => 'include',
1778 'target' => 'post_type',
1779 'operator' => 'in',
1780 'value' => ['page'],
1781 ],
1782 ],
1783 ];
1784 break;
1785 case 'search_results':
1786 $groups[] = [
1787 'relation' => 'AND',
1788 'rules' => [
1789 [
1790 'type' => 'include',
1791 'target' => 'search',
1792 'operator' => 'in',
1793 'value' => ['search'],
1794 ],
1795 ],
1796 ];
1797 break;
1798 case 'not_found':
1799 $groups[] = [
1800 'relation' => 'AND',
1801 'rules' => [
1802 [
1803 'type' => 'include',
1804 'target' => '404',
1805 'operator' => 'in',
1806 'value' => ['404'],
1807 ],
1808 ],
1809 ];
1810 break;
1811 default:
1812 $groups[] = [
1813 'relation' => 'AND',
1814 'rules' => [],
1815 ];
1816 break;
1817 }
1818
1819 return ['groups' => $groups];
1820 }
1821
1822 /**
1823 * Normalize template type based on selected sub-location.
1824 *
1825 * @param string $type Base type from UI.
1826 * @param string $sub_location Selected sub-location.
1827 *
1828 * @return string
1829 */
1830 private function normalize_type(string $type, string $sub_location): string
1831 {
1832 if (strpos($sub_location, 'archive') === 0) {
1833 return 'archive';
1834 }
1835
1836 if ('search_results' === $sub_location) {
1837 return 'search';
1838 }
1839
1840 if ('not_found' === $sub_location) {
1841 return 'not_found';
1842 }
1843
1844 if (strpos($sub_location, 'author') === 0) {
1845 return 'author';
1846 }
1847
1848 return $type ?: 'single';
1849 }
1850
1851 /**
1852 * Determine if sub-location is Pro-only.
1853 *
1854 * @param string $sub_location Sub-location identifier.
1855 *
1856 * @return bool
1857 */
1858 private function is_pro_location(string $sub_location): bool
1859 {
1860 if (in_array($sub_location, ['single_cpt', 'archive_tax', 'author_all', 'author_specific'], true)) {
1861 return true;
1862 }
1863
1864 if (strpos($sub_location, 'single_') === 0 && !in_array($sub_location, ['single_post', 'single_page'], true)) {
1865 return true;
1866 }
1867
1868 if (strpos($sub_location, 'archive_') === 0 && !in_array($sub_location, ['archive_blog', 'archive_category', 'archive_tag'], true)) {
1869 return true;
1870 }
1871
1872 return false;
1873 }
1874
1875 /**
1876 * Check whether Pro license is available.
1877 *
1878 * @return bool
1879 */
1880 private function can_use_pro(): bool
1881 {
1882 return function_exists('king_addons_freemius')
1883 && king_addons_freemius()->can_use_premium_code__premium_only();
1884 }
1885
1886 /**
1887 * Count enabled templates of a given primary type.
1888 *
1889 * @param string $type Primary type (single, archive, search, not_found, author).
1890 *
1891 * @return int
1892 */
1893 private function count_enabled_templates_by_type(string $type): int
1894 {
1895 $templates = $this->repository->get_active_templates();
1896 $count = 0;
1897
1898 foreach ($templates as $template) {
1899 if (empty($template['enabled'])) {
1900 continue;
1901 }
1902
1903 $template_location = $template['location'] ?? '';
1904
1905 if ($template_location === $type) {
1906 $count++;
1907 }
1908 }
1909
1910 return $count;
1911 }
1912
1913 /**
1914 * Prepare data for admin list table.
1915 *
1916 * @return array<int,array<string,mixed>>
1917 */
1918 private function prepare_admin_templates(): array
1919 {
1920 $templates = $this->repository->get_active_templates();
1921 $prepared = [];
1922
1923 foreach ($templates as $template) {
1924 $post = get_post($template['id']);
1925 if (!$post instanceof WP_Post) {
1926 continue;
1927 }
1928
1929 $prepared[] = [
1930 'id' => $template['id'],
1931 'title' => get_the_title($template['id']),
1932 'type' => $template['location'] ?? '',
1933 'sub_location' => $template['sub_location'] ?? '',
1934 'conditions' => $template['conditions'] ?? [],
1935 'priority' => $template['priority'] ?? 10,
1936 'enabled' => !empty($template['enabled']),
1937 'is_pro_only' => !empty($template['is_pro_only']),
1938 ];
1939 }
1940
1941 return $prepared;
1942 }
1943
1944 /**
1945 * Handle inline toggle/delete actions.
1946 *
1947 * @return void
1948 */
1949 private function handle_inline_actions(): void
1950 {
1951 $action = isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1952 $template_id = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1953
1954 if (!$template_id || empty($action)) {
1955 return;
1956 }
1957
1958 if ('delete_template' === $action) {
1959 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'ka_theme_builder_delete_' . $template_id)) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1960 return;
1961 }
1962 wp_trash_post($template_id);
1963 $this->repository->clear_cache();
1964 }
1965
1966 if ('disable_template' === $action || 'enable_template' === $action) {
1967 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'ka_theme_builder_toggle_' . $template_id)) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1968 return;
1969 }
1970 update_post_meta($template_id, Meta_Keys::ENABLED, 'enable_template' === $action ? '1' : '0');
1971 $this->repository->clear_cache();
1972 }
1973 }
1974
1975 /**
1976 * Check if Woo Builder should own the context.
1977 *
1978 * @return bool
1979 */
1980 private function is_woo_context(): bool
1981 {
1982 if (!class_exists(Woo_Context::class)) {
1983 return false;
1984 }
1985
1986 $context = Woo_Context::detect_context();
1987
1988 return in_array($context, ['single_product', 'product_archive', 'cart', 'checkout', 'my_account'], true);
1989 }
1990 }
1991
1992
1993
1994
1995