PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.76
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.76
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 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Theme_Builder / Theme_Builder.php

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

1,992 lines 75.7 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 }
1219 /* (Old table/modal styles removed; Theme Builder now mirrors Woo Builder UI) */
1220 </style>
1221 <?php
1222 }
1223
1224 /**
1225 * Enqueue front-end assets placeholder.
1226 *
1227 * @return void
1228 */
1229 public function enqueue_front_assets(): void
1230 {
1231 // Reserved for future front-end assets; keep file reference valid.
1232 }
1233
1234 /**
1235 * Enqueue preview handler for Elementor editor.
1236 *
1237 * @return void
1238 */
1239 public function enqueue_preview_handler(): void
1240 {
1241 $post_id = get_the_ID() ? (int) get_the_ID() : get_queried_object_id();
1242
1243 if (!$post_id || 'elementor_library' !== get_post_type($post_id)) {
1244 return;
1245 }
1246
1247 if (!get_post_meta($post_id, Meta_Keys::LOCATION, true)) {
1248 return;
1249 }
1250
1251 wp_enqueue_script(
1252 KING_ADDONS_ASSETS_UNIQUE_KEY . '-theme-builder-preview',
1253 KING_ADDONS_URL . 'includes/extensions/Theme_Builder/preview-handler.js',
1254 ['jquery'],
1255 KING_ADDONS_VERSION,
1256 true
1257 );
1258 }
1259
1260 /**
1261 * Register Elementor document controls for Theme Builder templates.
1262 *
1263 * @param mixed $document Document instance.
1264 *
1265 * @return void
1266 */
1267 public function register_document_controls($document): void
1268 {
1269 if (!is_object($document) || !method_exists($document, 'get_main_id')) {
1270 return;
1271 }
1272
1273 $post_id = (int) $document->get_main_id();
1274 if ('elementor_library' !== get_post_type($post_id)) {
1275 return;
1276 }
1277 $location = get_post_meta($post_id, Meta_Keys::LOCATION, true);
1278
1279 if (!$location) {
1280 return;
1281 }
1282
1283 $sub_location = get_post_meta($post_id, Meta_Keys::SUB_LOCATION, true);
1284 $preview_post = (int) get_post_meta($post_id, Meta_Keys::PREVIEW_POST_ID, true);
1285 $preview_term = (int) get_post_meta($post_id, Meta_Keys::PREVIEW_TERM_ID, true);
1286 $preview_author = (int) get_post_meta($post_id, Meta_Keys::PREVIEW_AUTHOR_ID, true);
1287 $preview_query = get_post_meta($post_id, Meta_Keys::PREVIEW_QUERY, true);
1288
1289 $document->start_controls_section(
1290 'ka_theme_builder_settings',
1291 [
1292 'label' => esc_html__('Theme Builder', 'king-addons'),
1293 'tab' => Controls_Manager::TAB_SETTINGS,
1294 ]
1295 );
1296
1297 $document->add_control(
1298 'ka_tb_readonly_type',
1299 [
1300 'label' => esc_html__('Template Type', 'king-addons'),
1301 'type' => Controls_Manager::RAW_HTML,
1302 'raw' => '<strong>' . esc_html($location) . '</strong>',
1303 'content_classes' => 'ka-theme-builder-readonly',
1304 ]
1305 );
1306
1307 $document->add_control(
1308 'ka_tb_readonly_location',
1309 [
1310 'label' => esc_html__('Location', 'king-addons'),
1311 'type' => Controls_Manager::RAW_HTML,
1312 'raw' => '<strong>' . esc_html($sub_location) . '</strong>',
1313 'content_classes' => 'ka-theme-builder-readonly',
1314 ]
1315 );
1316
1317 $document->add_control(
1318 'ka_tb_preview_post',
1319 [
1320 'label' => esc_html__('Preview Post ID', 'king-addons'),
1321 'type' => Controls_Manager::NUMBER,
1322 'default' => $preview_post,
1323 ]
1324 );
1325
1326 $document->add_control(
1327 'ka_tb_preview_term',
1328 [
1329 'label' => esc_html__('Preview Term ID', 'king-addons'),
1330 'type' => Controls_Manager::NUMBER,
1331 'default' => $preview_term,
1332 ]
1333 );
1334
1335 $document->add_control(
1336 'ka_tb_preview_author',
1337 [
1338 'label' => esc_html__('Preview Author ID', 'king-addons'),
1339 'type' => Controls_Manager::NUMBER,
1340 'default' => $preview_author,
1341 ]
1342 );
1343
1344 $document->add_control(
1345 'ka_tb_preview_query',
1346 [
1347 'label' => esc_html__('Preview Search Query', 'king-addons'),
1348 'type' => Controls_Manager::TEXT,
1349 'default' => $preview_query,
1350 ]
1351 );
1352
1353 if ($this->is_pro_location($sub_location)) {
1354 $document->add_control(
1355 'ka_tb_pro_notice',
1356 [
1357 'label' => esc_html__('Pro', 'king-addons'),
1358 'type' => Controls_Manager::RAW_HTML,
1359 'raw' => esc_html__('Advanced conditions require Pro.', 'king-addons'),
1360 'content_classes' => 'ka-theme-builder-readonly',
1361 ]
1362 );
1363 }
1364
1365 $document->end_controls_section();
1366 }
1367
1368 /**
1369 * Persist preview meta from Elementor document settings.
1370 *
1371 * @param \Elementor\Core\Base\Document $document Elementor document.
1372 * @param array<string,mixed> $data Saved data.
1373 *
1374 * @return void
1375 */
1376 public function handle_document_after_save($document, array $data): void
1377 {
1378 if (!is_object($document) || !method_exists($document, 'get_main_id')) {
1379 return;
1380 }
1381
1382 $post_id = (int) $document->get_main_id();
1383 if ('elementor_library' !== get_post_type($post_id)) {
1384 return;
1385 }
1386
1387 if (!get_post_meta($post_id, Meta_Keys::LOCATION, true)) {
1388 return;
1389 }
1390
1391 $preview_post = (int) $document->get_settings('ka_tb_preview_post');
1392 $preview_term = (int) $document->get_settings('ka_tb_preview_term');
1393 $preview_author = (int) $document->get_settings('ka_tb_preview_author');
1394 $preview_query = sanitize_text_field((string) $document->get_settings('ka_tb_preview_query'));
1395
1396 update_post_meta($post_id, Meta_Keys::PREVIEW_POST_ID, $preview_post);
1397 update_post_meta($post_id, Meta_Keys::PREVIEW_TERM_ID, $preview_term);
1398 update_post_meta($post_id, Meta_Keys::PREVIEW_AUTHOR_ID, $preview_author);
1399 update_post_meta($post_id, Meta_Keys::PREVIEW_QUERY, $preview_query);
1400
1401 $this->repository->clear_cache();
1402 }
1403
1404 /**
1405 * Handle "Add New" submission.
1406 *
1407 * @return void
1408 */
1409 public function handle_create_template(): void
1410 {
1411 if (!current_user_can('manage_options')) {
1412 wp_die(esc_html__('You do not have permission to perform this action.', 'king-addons'));
1413 }
1414
1415 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
1416 wp_die(esc_html__('Invalid nonce.', 'king-addons'));
1417 }
1418
1419 $title = isset($_POST['ka_tb_title']) ? sanitize_text_field(wp_unslash($_POST['ka_tb_title'])) : esc_html__('New Theme Template', 'king-addons');
1420 $type = isset($_POST['ka_tb_type']) ? sanitize_text_field(wp_unslash($_POST['ka_tb_type'])) : 'single';
1421 $sub_location = isset($_POST['ka_tb_location']) ? sanitize_text_field(wp_unslash($_POST['ka_tb_location'])) : '';
1422 $type = $this->normalize_type($type, $sub_location);
1423
1424 // Check Free tier limit: 1 active template per primary type
1425 if (!$this->can_use_pro()) {
1426 $primary_type = $this->normalize_type($type, $sub_location);
1427 if ($this->count_enabled_templates_by_type($primary_type) >= 1) {
1428 wp_die(
1429 sprintf(
1430 /* translators: %s: template type name */
1431 esc_html__('Free version allows only 1 active %s template. Disable an existing template or upgrade to Pro for unlimited templates.', 'king-addons'),
1432 esc_html($primary_type)
1433 )
1434 );
1435 }
1436 }
1437
1438 $post_id = wp_insert_post([
1439 'post_type' => 'elementor_library',
1440 'post_status' => 'publish',
1441 'post_title' => $title,
1442 ]);
1443
1444 if (is_wp_error($post_id)) {
1445 wp_die(esc_html__('Unable to create template.', 'king-addons'));
1446 }
1447
1448 $conditions = $this->build_default_conditions($sub_location);
1449
1450 $is_pro = Conditions::is_pro_only($conditions) || $this->is_pro_location($sub_location);
1451 $enabled = $is_pro && !$this->can_use_pro() ? '0' : '1';
1452
1453 update_post_meta($post_id, Meta_Keys::ENABLED, $enabled);
1454 update_post_meta($post_id, Meta_Keys::LOCATION, $type);
1455 update_post_meta($post_id, Meta_Keys::SUB_LOCATION, $sub_location);
1456 update_post_meta($post_id, Meta_Keys::PRIORITY, 10);
1457 update_post_meta($post_id, Meta_Keys::IS_PRO_ONLY, $is_pro ? '1' : '0');
1458 update_post_meta($post_id, Meta_Keys::CONDITIONS, wp_json_encode($conditions));
1459
1460 $this->repository->clear_cache();
1461
1462 wp_safe_redirect(admin_url('post.php?post=' . $post_id . '&action=elementor'));
1463 exit;
1464 }
1465
1466 /**
1467 * Handle quick update submission.
1468 *
1469 * @return void
1470 */
1471 public function handle_quick_update(): void
1472 {
1473 if (!current_user_can('manage_options')) {
1474 wp_die(esc_html__('You do not have permission to perform this action.', 'king-addons'));
1475 }
1476
1477 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
1478 wp_die(esc_html__('Invalid nonce.', 'king-addons'));
1479 }
1480
1481 $template_id = isset($_POST['template_id']) ? (int) $_POST['template_id'] : 0;
1482 $priority = isset($_POST['priority']) ? (int) $_POST['priority'] : 10;
1483 $enabled = isset($_POST['enabled']) && '1' === $_POST['enabled']; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1484
1485 if ($template_id > 0) {
1486 update_post_meta($template_id, Meta_Keys::PRIORITY, $priority);
1487 update_post_meta($template_id, Meta_Keys::ENABLED, $enabled ? '1' : '0');
1488 $this->repository->clear_cache();
1489 }
1490
1491 wp_safe_redirect(admin_url('admin.php?page=' . $this->menu_slug));
1492 exit;
1493 }
1494
1495 /**
1496 * Handle template duplication (Pro feature).
1497 *
1498 * @return void
1499 */
1500 public function handle_duplicate_template(): void
1501 {
1502 if (!current_user_can('manage_options')) {
1503 wp_die(esc_html__('You do not have permission to perform this action.', 'king-addons'));
1504 }
1505
1506 if (!$this->can_use_pro()) {
1507 wp_die(esc_html__('Template duplication requires Pro.', 'king-addons'));
1508 }
1509
1510 $template_id = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0;
1511
1512 if (!$template_id || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'ka_theme_builder_duplicate_' . $template_id)) {
1513 wp_die(esc_html__('Invalid request.', 'king-addons'));
1514 }
1515
1516 $original = get_post($template_id);
1517 if (!$original || 'elementor_library' !== $original->post_type) {
1518 wp_die(esc_html__('Template not found.', 'king-addons'));
1519 }
1520
1521 // Clone the post
1522 $new_post_id = wp_insert_post([
1523 'post_type' => 'elementor_library',
1524 'post_status' => 'draft',
1525 'post_title' => $original->post_title . ' ' . esc_html__('(Copy)', 'king-addons'),
1526 'post_content' => $original->post_content,
1527 ]);
1528
1529 if (is_wp_error($new_post_id)) {
1530 wp_die(esc_html__('Failed to duplicate template.', 'king-addons'));
1531 }
1532
1533 // Copy all meta data
1534 $meta_keys = [
1535 Meta_Keys::ENABLED,
1536 Meta_Keys::LOCATION,
1537 Meta_Keys::SUB_LOCATION,
1538 Meta_Keys::CONDITIONS,
1539 Meta_Keys::PRIORITY,
1540 Meta_Keys::IS_PRO_ONLY,
1541 Meta_Keys::PREVIEW_POST_ID,
1542 Meta_Keys::PREVIEW_TERM_ID,
1543 Meta_Keys::PREVIEW_AUTHOR_ID,
1544 Meta_Keys::PREVIEW_QUERY,
1545 ];
1546
1547 foreach ($meta_keys as $key) {
1548 $value = get_post_meta($template_id, $key, true);
1549 if ('' !== $value) {
1550 update_post_meta($new_post_id, $key, $value);
1551 }
1552 }
1553
1554 // Disable the duplicate by default to avoid conflicts
1555 update_post_meta($new_post_id, Meta_Keys::ENABLED, '0');
1556
1557 // Copy Elementor data
1558 $elementor_data = get_post_meta($template_id, '_elementor_data', true);
1559 if ($elementor_data) {
1560 update_post_meta($new_post_id, '_elementor_data', $elementor_data);
1561 }
1562
1563 $elementor_edit_mode = get_post_meta($template_id, '_elementor_edit_mode', true);
1564 if ($elementor_edit_mode) {
1565 update_post_meta($new_post_id, '_elementor_edit_mode', $elementor_edit_mode);
1566 }
1567
1568 $this->repository->clear_cache();
1569
1570 wp_safe_redirect(admin_url('admin.php?page=' . $this->menu_slug . '&duplicated=1'));
1571 exit;
1572 }
1573
1574 /**
1575 * Handle template include override.
1576 *
1577 * @param string $template Current template path.
1578 *
1579 * @return string
1580 */
1581 public function handle_template_include(string $template): string
1582 {
1583 if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) {
1584 return $template;
1585 }
1586
1587 if (!did_action('elementor/loaded')) {
1588 return $template;
1589 }
1590
1591 if (is_singular('elementor_library')) {
1592 return $template;
1593 }
1594
1595 if ($this->is_woo_context()) {
1596 return $template;
1597 }
1598
1599 $context = Context::from_request();
1600 $resolved = $this->resolver->resolve($context);
1601
1602 if (!$resolved) {
1603 return $template;
1604 }
1605
1606 $this->current_template_id = $resolved;
1607
1608 return KING_ADDONS_PATH . 'includes/extensions/Theme_Builder/templates/theme-builder.php';
1609 }
1610
1611 /**
1612 * Provide current template ID to filters.
1613 *
1614 * @param int $template_id Existing template id.
1615 *
1616 * @return int
1617 */
1618 public function filter_current_template_id(int $template_id): int
1619 {
1620 if ($this->current_template_id) {
1621 return (int) $this->current_template_id;
1622 }
1623
1624 return (int) $template_id;
1625 }
1626
1627 /**
1628 * Append body class for resolved template.
1629 *
1630 * @param array<int,string> $classes Body classes.
1631 *
1632 * @return array<int,string>
1633 */
1634 public function filter_body_class(array $classes): array
1635 {
1636 if ($this->current_template_id) {
1637 $classes[] = 'king-addons-theme-builder-active';
1638 }
1639
1640 return $classes;
1641 }
1642
1643 /**
1644 * Render add-new modal markup.
1645 *
1646 * @return void
1647 */
1648 private function render_add_new_modal(): void
1649 {
1650 ?>
1651 <div id="ka-theme-builder-modal" class="ka-tb-modal-overlay" aria-hidden="true">
1652 <div class="ka-tb-modal" role="dialog" aria-modal="true" aria-labelledby="ka-tb-create-title">
1653 <h3 id="ka-tb-create-title"><?php echo esc_html__('Create Theme Template', 'king-addons'); ?></h3>
1654 <p class="ka-tb-modal-desc"><?php echo esc_html__('Choose the template type and where it should apply.', 'king-addons'); ?></p>
1655
1656 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
1657 <input type="hidden" name="action" value="ka_theme_builder_create" />
1658 <?php wp_nonce_field('ka_theme_builder_create', 'ka_theme_builder_create_nonce'); ?>
1659
1660 <div class="ka-tb-form-group">
1661 <label class="ka-tb-form-label" for="ka-tb-title"><?php echo esc_html__('Template Name', 'king-addons'); ?></label>
1662 <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'); ?>" />
1663 </div>
1664
1665 <div class="ka-tb-form-group">
1666 <label class="ka-tb-form-label" for="ka-tb-type"><?php echo esc_html__('Template Type', 'king-addons'); ?></label>
1667 <select id="ka-tb-type" name="ka_tb_type" class="ka-tb-form-select">
1668 <option value="single"><?php echo esc_html__('Single', 'king-addons'); ?></option>
1669 <option value="archive"><?php echo esc_html__('Archive', 'king-addons'); ?></option>
1670 <option value="author"><?php echo esc_html__('Author', 'king-addons'); ?></option>
1671 <option value="search"><?php echo esc_html__('Search', 'king-addons'); ?></option>
1672 <option value="not_found"><?php echo esc_html__('404', 'king-addons'); ?></option>
1673 </select>
1674 </div>
1675
1676 <div class="ka-tb-form-group">
1677 <label class="ka-tb-form-label" for="ka-tb-location"><?php echo esc_html__('Location', 'king-addons'); ?></label>
1678 <select id="ka-tb-location" name="ka_tb_location" class="ka-tb-form-select">
1679 <option value="single_post"><?php echo esc_html__('All Posts', 'king-addons'); ?></option>
1680 <option value="single_page"><?php echo esc_html__('All Pages', 'king-addons'); ?></option>
1681 <option value="single_cpt"><?php echo esc_html__('Custom Post Types (Pro)', 'king-addons'); ?></option>
1682 <option value="archive_blog"><?php echo esc_html__('Blog / Posts Page', 'king-addons'); ?></option>
1683 <option value="archive_category"><?php echo esc_html__('All Categories', 'king-addons'); ?></option>
1684 <option value="archive_tag"><?php echo esc_html__('All Tags', 'king-addons'); ?></option>
1685 <option value="archive_tax"><?php echo esc_html__('Custom Taxonomy (Pro)', 'king-addons'); ?></option>
1686 <option value="author_all"><?php echo esc_html__('All Authors (Pro)', 'king-addons'); ?></option>
1687 <option value="author_specific"><?php echo esc_html__('Specific Authors (Pro)', 'king-addons'); ?></option>
1688 <option value="search_results"><?php echo esc_html__('Search Results', 'king-addons'); ?></option>
1689 <option value="not_found"><?php echo esc_html__('404 Page', 'king-addons'); ?></option>
1690 </select>
1691 </div>
1692
1693 <div class="ka-tb-modal-actions">
1694 <button type="button" class="ka-tb-btn ka-tb-btn-secondary ka-tb-modal-close"><?php echo esc_html__('Cancel', 'king-addons'); ?></button>
1695 <button type="submit" class="ka-tb-btn ka-tb-btn-primary"><?php echo esc_html__('Create and Edit with Elementor', 'king-addons'); ?></button>
1696 </div>
1697 </form>
1698 </div>
1699 </div>
1700 <?php
1701 }
1702
1703 /**
1704 * Render quick edit modal markup.
1705 *
1706 * @return void
1707 */
1708 private function render_quick_edit_modal(): void
1709 {
1710 ?>
1711 <div id="ka-theme-builder-quick-modal" class="ka-tb-modal-overlay" aria-hidden="true">
1712 <div class="ka-tb-modal" role="dialog" aria-modal="true" aria-labelledby="ka-tb-quick-title">
1713 <h3 id="ka-tb-quick-title"><?php echo esc_html__('Quick Edit Template', 'king-addons'); ?></h3>
1714 <p class="ka-tb-modal-desc"><?php echo esc_html__('Change priority and enabled status.', 'king-addons'); ?></p>
1715
1716 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
1717 <input type="hidden" name="action" value="ka_theme_builder_quick_update" />
1718 <?php wp_nonce_field('ka_theme_builder_quick', 'ka_theme_builder_quick_nonce'); ?>
1719 <input type="hidden" id="ka-tb-quick-template-id" name="template_id" value="" />
1720
1721 <div class="ka-tb-form-group">
1722 <label class="ka-tb-form-label" for="ka-tb-quick-priority"><?php echo esc_html__('Priority', 'king-addons'); ?></label>
1723 <input type="number" id="ka-tb-quick-priority" name="priority" min="0" step="1" value="10" class="ka-tb-modal-input" />
1724 </div>
1725
1726 <div class="ka-tb-form-group">
1727 <label class="ka-tb-form-label" for="ka-tb-quick-enabled"><?php echo esc_html__('Status', 'king-addons'); ?></label>
1728 <select id="ka-tb-quick-enabled" name="enabled" class="ka-tb-form-select">
1729 <option value="1"><?php echo esc_html__('Enabled', 'king-addons'); ?></option>
1730 <option value="0"><?php echo esc_html__('Disabled', 'king-addons'); ?></option>
1731 </select>
1732 </div>
1733
1734 <div class="ka-tb-modal-actions">
1735 <button type="button" class="ka-tb-btn ka-tb-btn-secondary ka-tb-modal-close"><?php echo esc_html__('Cancel', 'king-addons'); ?></button>
1736 <button type="submit" class="ka-tb-btn ka-tb-btn-primary"><?php echo esc_html__('Save', 'king-addons'); ?></button>
1737 </div>
1738 </form>
1739 </div>
1740 </div>
1741 <?php
1742 }
1743
1744 /**
1745 * Build default conditions for a selected location.
1746 *
1747 * @param string $sub_location Sub-location value.
1748 *
1749 * @return array<string,mixed>
1750 */
1751 private function build_default_conditions(string $sub_location): array
1752 {
1753 $groups = [];
1754
1755 switch ($sub_location) {
1756 case 'single_post':
1757 $groups[] = [
1758 'relation' => 'AND',
1759 'rules' => [
1760 [
1761 'type' => 'include',
1762 'target' => 'post_type',
1763 'operator' => 'in',
1764 'value' => ['post'],
1765 ],
1766 ],
1767 ];
1768 break;
1769 case 'single_page':
1770 $groups[] = [
1771 'relation' => 'AND',
1772 'rules' => [
1773 [
1774 'type' => 'include',
1775 'target' => 'post_type',
1776 'operator' => 'in',
1777 'value' => ['page'],
1778 ],
1779 ],
1780 ];
1781 break;
1782 case 'search_results':
1783 $groups[] = [
1784 'relation' => 'AND',
1785 'rules' => [
1786 [
1787 'type' => 'include',
1788 'target' => 'search',
1789 'operator' => 'in',
1790 'value' => ['search'],
1791 ],
1792 ],
1793 ];
1794 break;
1795 case 'not_found':
1796 $groups[] = [
1797 'relation' => 'AND',
1798 'rules' => [
1799 [
1800 'type' => 'include',
1801 'target' => '404',
1802 'operator' => 'in',
1803 'value' => ['404'],
1804 ],
1805 ],
1806 ];
1807 break;
1808 default:
1809 $groups[] = [
1810 'relation' => 'AND',
1811 'rules' => [],
1812 ];
1813 break;
1814 }
1815
1816 return ['groups' => $groups];
1817 }
1818
1819 /**
1820 * Normalize template type based on selected sub-location.
1821 *
1822 * @param string $type Base type from UI.
1823 * @param string $sub_location Selected sub-location.
1824 *
1825 * @return string
1826 */
1827 private function normalize_type(string $type, string $sub_location): string
1828 {
1829 if (strpos($sub_location, 'archive') === 0) {
1830 return 'archive';
1831 }
1832
1833 if ('search_results' === $sub_location) {
1834 return 'search';
1835 }
1836
1837 if ('not_found' === $sub_location) {
1838 return 'not_found';
1839 }
1840
1841 if (strpos($sub_location, 'author') === 0) {
1842 return 'author';
1843 }
1844
1845 return $type ?: 'single';
1846 }
1847
1848 /**
1849 * Determine if sub-location is Pro-only.
1850 *
1851 * @param string $sub_location Sub-location identifier.
1852 *
1853 * @return bool
1854 */
1855 private function is_pro_location(string $sub_location): bool
1856 {
1857 if (in_array($sub_location, ['single_cpt', 'archive_tax', 'author_all', 'author_specific'], true)) {
1858 return true;
1859 }
1860
1861 if (strpos($sub_location, 'single_') === 0 && !in_array($sub_location, ['single_post', 'single_page'], true)) {
1862 return true;
1863 }
1864
1865 if (strpos($sub_location, 'archive_') === 0 && !in_array($sub_location, ['archive_blog', 'archive_category', 'archive_tag'], true)) {
1866 return true;
1867 }
1868
1869 return false;
1870 }
1871
1872 /**
1873 * Check whether Pro license is available.
1874 *
1875 * @return bool
1876 */
1877 private function can_use_pro(): bool
1878 {
1879 return function_exists('king_addons_freemius')
1880 && king_addons_freemius()->can_use_premium_code__premium_only();
1881 }
1882
1883 /**
1884 * Count enabled templates of a given primary type.
1885 *
1886 * @param string $type Primary type (single, archive, search, not_found, author).
1887 *
1888 * @return int
1889 */
1890 private function count_enabled_templates_by_type(string $type): int
1891 {
1892 $templates = $this->repository->get_active_templates();
1893 $count = 0;
1894
1895 foreach ($templates as $template) {
1896 if (empty($template['enabled'])) {
1897 continue;
1898 }
1899
1900 $template_location = $template['location'] ?? '';
1901
1902 if ($template_location === $type) {
1903 $count++;
1904 }
1905 }
1906
1907 return $count;
1908 }
1909
1910 /**
1911 * Prepare data for admin list table.
1912 *
1913 * @return array<int,array<string,mixed>>
1914 */
1915 private function prepare_admin_templates(): array
1916 {
1917 $templates = $this->repository->get_active_templates();
1918 $prepared = [];
1919
1920 foreach ($templates as $template) {
1921 $post = get_post($template['id']);
1922 if (!$post instanceof WP_Post) {
1923 continue;
1924 }
1925
1926 $prepared[] = [
1927 'id' => $template['id'],
1928 'title' => get_the_title($template['id']),
1929 'type' => $template['location'] ?? '',
1930 'sub_location' => $template['sub_location'] ?? '',
1931 'conditions' => $template['conditions'] ?? [],
1932 'priority' => $template['priority'] ?? 10,
1933 'enabled' => !empty($template['enabled']),
1934 'is_pro_only' => !empty($template['is_pro_only']),
1935 ];
1936 }
1937
1938 return $prepared;
1939 }
1940
1941 /**
1942 * Handle inline toggle/delete actions.
1943 *
1944 * @return void
1945 */
1946 private function handle_inline_actions(): void
1947 {
1948 $action = isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1949 $template_id = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1950
1951 if (!$template_id || empty($action)) {
1952 return;
1953 }
1954
1955 if ('delete_template' === $action) {
1956 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'ka_theme_builder_delete_' . $template_id)) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1957 return;
1958 }
1959 wp_trash_post($template_id);
1960 $this->repository->clear_cache();
1961 }
1962
1963 if ('disable_template' === $action || 'enable_template' === $action) {
1964 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'ka_theme_builder_toggle_' . $template_id)) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1965 return;
1966 }
1967 update_post_meta($template_id, Meta_Keys::ENABLED, 'enable_template' === $action ? '1' : '0');
1968 $this->repository->clear_cache();
1969 }
1970 }
1971
1972 /**
1973 * Check if Woo Builder should own the context.
1974 *
1975 * @return bool
1976 */
1977 private function is_woo_context(): bool
1978 {
1979 if (!class_exists(Woo_Context::class)) {
1980 return false;
1981 }
1982
1983 $context = Woo_Context::detect_context();
1984
1985 return in_array($context, ['single_product', 'product_archive', 'cart', 'checkout', 'my_account'], true);
1986 }
1987 }
1988
1989
1990
1991
1992