PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
← All changes | includes/Core/Admin.php +384 -58 3.0.63.8.0 View file →
@@ -4,14 +4,17 @@
4 4 use Exception;
5 5 use PriyoMukul\WPNotice\Notices;
6 6 use PriyoMukul\WPNotice\Utils\CacheBank;
7 7 use PriyoMukul\WPNotice\Utils\NoticeRemover;
8 -use Templately\API\Login;
8 +use Templately\Modules\Auth\REST\Login;
9 +use Templately\Modules\FullSiteImport\FullSiteImport;
10 +use Templately\Admin\Roles;
11 +use Templately\Core\Capabilities;
9 12 use Templately\Utils\Base;
10 13 use Templately\Utils\Helper;
11 14 use Templately\Utils\Options;
12 -use Templately\Core\Platform\Elementor;
13 -use Templately\Core\Platform\Gutenberg;
15 +use Templately\Modules\ElementorIntegration\Platform\Elementor;
16 +use Templately\Modules\GutenbergIntegration\Platform\Gutenberg;
14 17
15 18 class Admin extends Base {
16 19
17 20 /**
@@ -24,8 +27,9 @@
24 27 * Menu, Assets and maybe redirect on plugin activation is initialized.
25 28 */
26 29 public function __construct() {
27 30 add_action( 'admin_enqueue_scripts', [ $this, 'scripts' ] );
31 + add_action( 'load-toplevel_page_templately', [ $this, 'before_scripts' ] );
28 32 add_action( 'admin_menu', [ $this, 'admin_menu' ] );
29 33
30 34 self::$cache_bank = CacheBank::get_instance();
31 35
@@ -34,8 +38,14 @@
34 38 } catch ( Exception $e ) {
35 39 unset( $e );
36 40 }
37 41
42 + add_filter( 'plugin_action_links_' . TEMPLATELY_PLUGIN_BASENAME, [$this, 'handleActionLinks'], 10, 2 );
43 +
44 + // Post-import feedback mount point moved to
45 + // modules/post-import-feedback/Widget.php::render_mount_point() (spec 027) —
46 + // hooked independently on admin_footer.
47 +
38 48 // Remove OLD notice from 1.0.0 (if other WPDeveloper plugin has notice)
39 49 NoticeRemover::get_instance( '1.0.0' );
40 50 }
41 51
@@ -74,9 +84,14 @@
74 84 'show_ui' => true,
75 85 'show_in_menu' => false,
76 86 'query_var' => true,
77 87 'rewrite' => [ 'slug' => 'templately-library' ],
88 + // Kept in sync with the theme-builder module's own registration of this
89 + // same post type (modules/theme-builder/Source.php) — whichever runs last
90 + // wins, so both must carry the administrator-only capability map.
78 91 'capability_type' => 'post',
92 + 'capabilities' => Roles::builder_post_type_capabilities(),
93 + 'map_meta_cap' => true,
79 94 'has_archive' => true,
80 95 'hierarchical' => false,
81 96 'menu_position' => 10,
82 97 'supports' => [ 'title', 'editor', 'author', 'thumbnail', 'custom-field' ],
@@ -84,8 +99,12 @@
84 99
85 100 register_post_type( 'templately_library', $args );
86 101 }
87 102
103 + public function before_scripts() {
104 + header("x-frame-options: ALLOW-FROM *.templately.com");
105 + }
106 +
88 107 /**
89 108 * Enqueuing Assets
90 109 *
91 110 * @param string $hook
@@ -90,13 +109,62 @@
90 109 *
91 110 * @param string $hook
92 111 * @return void
93 112 */
94 - public function scripts( string $hook ) {
95 - if ( ! in_array( $hook, [ 'edit.php', 'toplevel_page_templately', 'elementor', 'gutenberg' ], true ) ) {
113 + public function scripts( $hook = '' ) {
114 + $allowed_hooks = apply_filters(
115 + 'templately_admin_enqueue_allowed_hooks',
116 + [ 'index.php', 'edit.php', 'toplevel_page_templately', 'elementor', 'gutenberg', 'templately_page_templately_settings' ],
117 + $hook
118 + );
119 +
120 + if ( !empty($hook) && ! in_array( $hook, $allowed_hooks, true ) ) {
96 121 return;
97 122 }
98 123
124 + $templately_settings = [];
125 + // if('templately_page_templately_settings' === $hook){
126 + $kit = null;
127 + $el_settings = [];
128 + if(defined('ELEMENTOR_VERSION')){
129 + $kit = \Elementor\Plugin::$instance->kits_manager->get_active_kit();
130 + if($kit){
131 + $el_settings = $kit->get_settings();
132 + }
133 + }
134 + $site_logo_id = get_theme_mod( 'custom_logo' );
135 + $eb_settings = get_option('eb_global_styles', []);
136 + $eb_settings = is_array($eb_settings) ? $eb_settings: [];
137 + $eb_settings = array_map(function($item) { return json_decode($item, true); }, $eb_settings);
138 + $site_logo = [
139 + 'id' => $site_logo_id,
140 + 'url' => $site_logo_id ? wp_get_attachment_url($site_logo_id) : '',
141 + // 'size' => (int) get_option('templately_site_logo_size', ''),
142 + ];
143 +
144 + $templately_settings = [
145 + 'nonce' => wp_create_nonce( 'templately_nonce' ),
146 + 'has_revert' => FullSiteImport::has_revert(),
147 + 'has_elementor' => $this->check_plugin_status( 'elementor/elementor.php' ),
148 + 'has_eb' => $this->check_plugin_status( 'essential-blocks/essential-blocks.php' ),
149 + 'site_logo' => $site_logo,
150 + 'elementor' => $el_settings,
151 + 'gutenberg' => $eb_settings,
152 + 'siteTitle' => get_option('blogname', ''),
153 + 'siteTagline' => get_option('blogdescription', ''),
154 + 'customCSS' => get_option('templately_custom_css', ''),
155 + // 'imported_platform' => get_option('templately_import_platform', ''),
156 + ];
157 + // }
158 +
159 + if('index.php' === $hook){
160 + // need separate condition so we can return if page is index.php
161 + // Post-import feedback widget enqueue/localize moved to
162 + // modules/post-import-feedback/Widget.php (spec 027) — hooked independently
163 + // on admin_enqueue_scripts.
164 + return;
165 + }
166 +
99 167 $templately = [];
100 168
101 169 if ( 'edit.php' === $hook ) {
102 170 global $current_screen;
@@ -122,71 +190,197 @@
122 190 'types' => $types
123 191 ];
124 192 }
125 193
126 - $script_dependencies = [];
194 + $script_dependencies = ['regenerator-runtime'];
127 195 $_localize_handle = 'templately';
128 196 $_current_screen = 'templately';
129 197
198 + // The webpack "commons" chunk (splitChunks) holds the react-src core + vendor code SHARED
199 + // across the main bundle AND every module bundle — extracted once instead of duplicated
200 + // into each. It MUST load before any of them. Making the main 'templately' handle depend
201 + // on it means every module bundle (which depends on 'templately') gets it transitively, in
202 + // the right order. Its own .asset.php lists no deps (DEWP doesn't attribute externals to
203 + // shared chunks), so declare the WordPress script globals its shared code references here.
204 + if ( file_exists( TEMPLATELY_PATH . 'assets/js/templately-commons.js' ) ) {
205 + templately()->assets->register( 'templately-commons', 'js/templately-commons.js', [
206 + 'react', 'react-dom', 'react-jsx-runtime', 'wp-element', 'wp-hooks', 'wp-i18n',
207 + 'wp-api-fetch', 'wp-url', 'wp-data', 'wp-components', 'lodash',
208 + ], true );
209 + $script_dependencies[] = 'templately-commons';
210 + }
211 +
130 212 if ( $hook === 'elementor' || $hook === 'gutenberg' ) {
131 - $_current_screen = $hook;
132 - $_localize_handle = 'templately-' . $hook;
133 - $script_dependencies = [$_localize_handle];
213 + $_current_screen = $hook;
214 + $_localize_handle = 'templately-' . $hook;
215 + $script_dependencies[] = $_localize_handle;
134 216 }
135 217
136 - if ( $hook === 'toplevel_page_templately' || $hook == 'edit.php' ) {
218 + if ( 'templately_page_templately_settings' === $hook ) {
219 + templately()->assets->register( 'settings-vendor', 'js/chunks/settings-vendor.js', [], true );
220 + $script_dependencies[] = 'settings-vendor';
221 + }
222 +
223 + if ( $hook === 'toplevel_page_templately' || $hook == 'edit.php' || 'templately_page_templately_settings' === $hook ) {
137 224 templately()->assets->enqueue( 'templately-admin', 'css/admin.css', [ 'templately' ] );
138 225 }
139 226
140 - // Google Font Enqueueing
227 + // Google Fonts Enqueueing
228 + // DM Sans - Used in general UI
141 229 templately()->assets->enqueue(
142 230 'templately-dmsans',
143 231 set_url_scheme( '//fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap' )
144 232 );
145 233
234 + // Inter - Used in Tailwind components and various UI elements
235 + templately()->assets->enqueue(
236 + 'templately-inter',
237 + set_url_scheme( '//fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap' )
238 + );
239 +
240 + // DM Mono - Used in settings and code-related UI
241 + templately()->assets->enqueue(
242 + 'templately-dmmono',
243 + set_url_scheme( '//fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap' )
244 + );
245 +
246 + wp_enqueue_style('wp-components');
247 +
248 + wp_enqueue_media();
146 249 templately()->assets->enqueue( 'templately', 'js/templately.js', $script_dependencies, true );
147 - templately()->assets->enqueue( 'templately', 'css/templately.css', ['templately-dmsans'] );
250 + // splitChunks moves the shared component CSS into the commons chunk's stylesheet
251 + // (templately-commons.css) and leaves the per-entry templately.css empty/absent. Register
252 + // the shared CSS under the SAME 'templately' style handle so every stylesheet that depends
253 + // on it (templately-tailwind, templately-admin) still resolves — WP won't enqueue a style
254 + // whose dependency handle is unregistered. Fall back to templately.css if a build didn't
255 + // split (no commons chunk).
256 + $main_css = file_exists( TEMPLATELY_PATH . 'assets/css/templately-commons.css' ) ? 'css/templately-commons.css' : 'css/templately.css';
257 + templately()->assets->enqueue( 'templately', $main_css, ['templately-dmsans', 'templately-inter'] );
148 258
259 + $tailwind_hooks = apply_filters( 'templately_tailwind_hooks', [ 'toplevel_page_templately', 'elementor', 'gutenberg', 'templately_page_templately_settings', 'edit.php' ] );
260 + if ( in_array( $hook, $tailwind_hooks, true ) ) {
261 + templately()->assets->enqueue( 'templately-tailwind', 'css/tailwind.css', ['templately', 'templately-inter'] );
262 + }
263 +
264 + // MODULE STYLES. @wordpress/scripts splits extracted CSS by ORIGIN, not by feature:
265 + // `{entry}.css` holds CSS reached from a stylesheet entry point, while
266 + // `style-{entry}.css` holds CSS `import`ed from JS. Every module's SCSS is imported from
267 + // its own index (e.g. modules/auth/assets/js/index.tsx imports ../scss/style.scss), so
268 + // with the modules folded into the `templately` graph ALL of it now lands in
269 + // style-templately.css — a file nothing enqueued.
270 + //
271 + // This is the regression that shipped the sign-in screen completely unstyled: the header
272 + // and banner came from templately.css and looked fine, so the page rendered but the
273 + // module-owned views did not. Before the merge these styles were emitted per module to
274 + // assets/css/modules/*.css and picked up by enqueue_module_assets()'s glob; that glob is
275 + // gone, and this is its replacement.
276 + //
277 + // ORDER MATTERS, and this must stay AFTER the Tailwind enqueue. The retired glob ran at
278 + // the very end of this method — after Tailwind — so module rules beat Tailwind utilities
279 + // on equal specificity. Enqueueing it earlier silently inverts that: the AI sidebar chat
280 + // bubble lost its overflow-wrap to a Tailwind utility and long URLs broke out of the
281 + // bubble (caught by 020-ai-chat-bubble-overflow.spec.js, not by eye).
282 + if ( file_exists( TEMPLATELY_PATH . 'assets/css/style-templately.css' ) ) {
283 + templately()->assets->enqueue( 'templately-modules', 'css/style-templately.css', [ 'templately' ] );
284 + }
285 +
286 + // Module assets are no longer enqueued separately: every feature module compiles into
287 + // the 'templately' bundle via react-src/generated/moduleManifest.ts (Constitution XIII).
288 + // The former enqueue_module_assets() globbed assets/js/modules/*.js and loaded all of
289 + // them unconditionally wherever this ran, so folding them into the main handle is a
290 + // parity change, not a scope change.
291 +
149 292 /**
150 293 * @var Elementor|Gutenberg $platform
151 294 */
152 295 $platform = $this->platform( $_current_screen );
153 296
297 + $global_user = Options::get_instance()->get( 'user', false, get_current_user_id() );
298 + $hide_buttons = get_option('templately-gutenberg-hide-buttons', 'no');
299 + $hide_buttons = $hide_buttons === 'yes' ? 'yes' : 'no';
300 +
154 301 $templately = array_merge( [
155 302 'url' => home_url(),
156 303 'site_url' => site_url(),
157 304 'nonce' => wp_create_nonce( 'templately_nonce' ),
305 + 'is_ssl' => is_ssl(),
158 306 'rest_args' => [
159 307 'nonce' => wp_create_nonce( 'wp_rest' ),
160 308 'endpoint' => get_rest_url( null, 'templately/v1/' )
161 309 ],
162 - 'log' => defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG,
163 - 'dev_mode' => defined( 'TEMPLATELY_DEV' ) && TEMPLATELY_DEV,
164 - "icons" => [
310 + "icons" => [
165 311 'construction' => templately()->assets->icon( 'icons/construction.gif' ),
166 312 'profile' => templately()->assets->icon( 'icons/profile.svg' ),
167 313 'warning' => templately()->assets->icon( 'icons/warning.png' )
168 314 ],
169 - 'promo_image' => templately()->assets->icon( 'single-page-promo.png' ),
170 - 'default_image' => templately()->assets->icon( 'clouds/cloud-item.svg' ),
171 - 'not_found' => templately()->assets->icon( 'no-item-found.png' ),
172 - 'no_items' => templately()->assets->icon( 'no-items.png' ),
173 - 'loadingImage' => templately()->assets->icon( 'logos/loading-logo.gif' ),
174 - 'current_url' => admin_url( 'admin.php?page=templately' ),
175 - 'is_signed' => Login::is_signed(),
176 - 'is_globally_signed' => Login::is_globally_signed(),
177 - 'signed_as_global' => Login::signed_as_global(),
178 - 'current_screen' => $_current_screen,
179 - 'post_type' => get_post_type(),
180 - 'has_elementor_pro' => rest_sanitize_boolean( is_plugin_active( 'elementor-pro/elementor-pro.php' ) ),
315 + 'locale' => get_locale(),
316 + 'promo_image' => templately()->assets->icon( 'single-page-promo.png' ),
317 + 'default_image' => templately()->assets->icon( 'clouds/cloud-item.svg' ),
318 + 'not_found' => templately()->assets->icon( 'no-item-found.png' ),
319 + 'no_items' => templately()->assets->icon( 'no-items.png' ),
320 + 'loadingImage' => templately()->assets->icon( 'logos/loading-logo.gif' ),
321 + 'current_url' => admin_url( 'admin.php?page=templately' ),
322 + 'wp_dashboard_url' => admin_url( 'index.php' ),
323 + 'templately_library_url' => admin_url( 'edit.php?post_type=templately_library' ),
324 + 'is_signed' => Login::is_signed(),
325 + 'is_globally_signed' => Login::is_globally_signed(),
326 + 'signed_as_global' => Login::signed_as_global(),
327 + 'current_screen' => $_current_screen,
328 + 'can_fsi' => current_user_can('install_plugins') && current_user_can('install_themes'),
329 + 'post_type' => get_post_type(),
330 + 'has_elementor' => rest_sanitize_boolean( is_plugin_active( 'elementor/elementor.php' ) ),
331 + 'has_elementor_pro' => rest_sanitize_boolean( is_plugin_active( 'elementor-pro/elementor-pro.php' ) ),
181 332 'theme' => $_current_screen == 'templately' ? 'light' : $platform->ui_theme(),
182 333 'is_wp_support_gutenberg' => version_compare( get_bloginfo( 'version' ), '5.0.0', '>=' ),
334 + 'hide_buttons' => $hide_buttons,
335 + 'settings' => $templately_settings,
336 + 'wp_user_avatar' => get_avatar_url( get_current_user_id(), [ 'size' => 96 ] ),
337 + 'allowed_mime_types' => get_allowed_mime_types(),
338 + 'is_free_user' => $global_user === false || isset( $global_user['plan'] ) && $global_user['plan'] == 'free',
339 + 'is_disconnected' => ! empty( $global_user['is_disconnected'] ),
340 + 'tour_status' => Options::get_instance()->get( 'tour_status', [] ),
341 + // Host-capability map (spec 053) — the ONE delivery point for every
342 + // Templately bundle (admin SPA and editor surfaces all localize here).
343 + // Always sourced from the registry, never hand-built (FR-025).
344 + 'caps' => Capabilities::get_instance()->get_map(),
345 + // Mirrors the account-scoped server gate so the client can hide what it
346 + // would only be 403'd for: Subscription (a plan purchase is made FOR the
347 + // account, and Checkout gates `subscription` on `manage_options`).
348 + // Hidden rather than shown-and-refused — offering a button that cannot
349 + // work is worse than not offering it.
350 + 'can_manage_account' => current_user_can( 'manage_options' ),
351 + // Which Templately site the plugin talks to. `templatelyDeveloper.dev_api`
352 + // carries the same flag, but only when developer mode is on and only from
353 + // its own script handle — so anything rendered before that handle runs (the
354 + // footer CTA, for one) linked to the live site while the plugin was pointed
355 + // at dev. Kept here, on the main localized object, `webURL()` is right from
356 + // the first paint. See `react-src/utils/helper.ts#webURL`.
357 + 'dev_api' => Helper::is_dev_api(),
183 358 ], $templately );
184 359
360 + // Apply filter to allow network admin modifications
361 + $templately = apply_filters( 'templately_admin_localized_data', $templately );
362 +
185 363 templately()->assets->localize( $_localize_handle, 'templately', $templately );
364 +
365 + do_action( 'templately_admin_scripts', $hook );
186 366 }
187 367
188 368 /**
369 + * Check the status of a plugin.
370 + *
371 + * @param string $plugin_path Plugin path relative to the plugins directory.
372 + * @return mixed true if active, false if not installed, 0 if installed but not active.
373 + */
374 + private function check_plugin_status( $plugin_path ) {
375 + $plugins = get_plugins();
376 + if ( isset( $plugins[ $plugin_path ] ) ) {
377 + return is_plugin_active( $plugin_path ) ? true : 0;
378 + }
379 + return false;
380 + }
381 +
382 + /**
189 383 * Admin notices for Review and others.
190 384 *
191 385 * @since 2.0.0
192 386 * @return void
@@ -194,8 +388,9 @@
194 388 * @since 2.0.0
195 389 */
196 390 public function notices() {
197 391 $notices = new Notices( [
392 + // 'dev_mode' => true, // also need to clear templately_notices from option/site meta table.
198 393 'id' => 'templately',
199 394 'storage_key' => 'notices',
200 395 'lifetime' => 3,
201 396 'stylesheet_url' => TEMPLATELY_ASSETS . 'css/notices.css',
@@ -238,9 +433,9 @@
238 433 'class' => 'dismiss-btn'
239 434 ]
240 435 ],
241 436 'support' => [
242 - 'link' => 'https://wpdeveloper.com/support',
437 + 'link' => Helper::web_url( '', [ 'support' => 'open' ] ),
243 438 'attributes' => [
244 439 'target' => '_blank'
245 440 ],
246 441 'label' => __( 'I need help', 'templately' ),
@@ -275,40 +470,100 @@
275 470 ] );
276 471 }
277 472
278 473 if ( $global_user === false || isset( $global_user['plan'] ) && $global_user['plan'] == 'free' ) {
279 - $notices->add( 'upsale', wp_sprintf( '<p>%1$s <a target="_blank" href="%3$s">%2$s</a>.</p>', __( '🔥 Get access to 5,000+ Ready Templates & save up to 65% OFF now', 'templately' ), __( 'Upgrade to Pro', 'templately' ), 'https://templately.com/#pricing' ), [
280 - 'start' => $notices->strtotime( '+10 day' ),
474 + $crown = '<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m15.743 10.938.161-1.578c.086-.842.142-1.399.098-1.749h.015c.727 0 1.316-.622 1.316-1.389s-.589-1.389-1.315-1.389c-.727 0-1.316.622-1.316 1.39 0 .346.12.663.32.907-.287.186-.66.58-1.223 1.171-.434.456-.65.684-.893.72a.7.7 0 0 1-.394-.059c-.223-.104-.372-.385-.67-.95l-1.57-2.97a22 22 0 0 0-.476-.873c.569-.306.958-.93.958-1.65C10.754 1.496 9.97.667 9 .667s-1.754.829-1.754 1.852c0 .72.389 1.344.958 1.65-.139.234-.293.525-.476.873l-1.57 2.97c-.298.565-.447.846-.67.95a.7.7 0 0 1-.394.058c-.242-.035-.46-.263-.893-.719-.563-.592-.937-.985-1.223-1.171.2-.244.32-.56.32-.908 0-.767-.589-1.389-1.316-1.389-.726 0-1.315.622-1.315 1.39 0 .766.589 1.388 1.315 1.388h.016c-.045.35.012.906.098 1.749l.16 1.578c.09.876.164 1.71.255 2.46H15.49c.09-.75.165-1.584.254-2.46m-7.698 6.395h1.908c2.488 0 3.732 0 4.562-.784.362-.342.591-.959.757-1.762H2.727c.166.803.395 1.42.757 1.762.83.784 2.074.784 4.562.784" fill="#fff"/></svg>';
475 +
476 + // $notices->add( 'upsale', wp_sprintf( '<p>%1$s <a target="_blank" href="%3$s">%2$s</a>.</p>', __( '🔥 Get access to 6,500+ Ready Templates & save up to 65% OFF now', 'templately' ), __( 'Upgrade to Pro', 'templately' ), 'https://templately.com/#pricing' ), [
477 + // 'start' => $notices->strtotime( '+10 day' ),
478 + // 'dismissible' => true,
479 + // 'refresh' => TEMPLATELY_VERSION,
480 + // 'screens' => [
481 + // 'dashboard',
482 + // 'plugins',
483 + // 'themes',
484 + // 'edit-page',
485 + // 'edit-post',
486 + // 'users',
487 + // 'tools',
488 + // 'options-general',
489 + // 'nav-menus'
490 + // ]
491 + // ] );
492 +
493 + // spring_savings_2026 notice
494 + $_notice_text_spring_savings_2026 = '<p style="margin-top: 0; margin-bottom: 0px;">🌸 Spring Savings: Design Website Quickly With 6,500+ Elementor & Gutenberg Templates, Including AI-Powered Features ⚡️ – Now <strong>Flat 50% OFF!</strong></p>';
495 +
496 + $_spring_savings_2026 = [
497 + 'thumbnail' => templately()->assets->icon( 'logos/logo-full.svg' ),
498 + 'html' => $_notice_text_spring_savings_2026,
499 + 'links' => [
500 + 'later' => [
501 + 'link' => Helper::web_url() . '#pricing',
502 + 'target' => '_blank',
503 + 'label' => __( 'Upgrade To Pro Now', 'templately' ),
504 + 'attributes' => [
505 + 'class' => 'button button-primary',
506 + ]
507 + ],
508 + 'never_show_again' => [
509 + 'label' => __( 'Maybe Later', 'templately' ),
510 + 'attributes' => [
511 + 'data-dismiss' => true,
512 + 'class' => 'button button-link dismiss-btn',
513 + ]
514 + ],
515 + ],
516 + ];
517 +
518 + $notices->add( 'spring_savings_2026', $_spring_savings_2026, [
519 + 'start' => $notices->strtotime('00:00:01AM 8th April, 2026'),
520 + 'recurrence' => false,
281 521 'dismissible' => true,
282 522 'refresh' => TEMPLATELY_VERSION,
523 + "expire" => strtotime( '11:59:59pm 10th May, 2026' ),
524 + 'display_if' => !wp_is_mobile(),
283 525 'screens' => [
284 526 'dashboard',
285 - 'plugins',
286 - 'themes',
287 - 'edit-page',
288 - 'edit-post',
289 - 'users',
290 - 'tools',
291 - 'options-general',
292 - 'nav-menus'
293 - ]
527 + ],
294 528 ] );
295 529
296 - $notice_text = '<p style="margin-top: 0; margin-bottom: 10px;">Black Friday Sale: Save up to 70% and <strong>get access to 5000+ ready templates</strong> to design amazing websites ✨</p>
297 - <a class="button button-primary" href="https://wpdeveloper.com/upgrade/templately-bfcm" target="_blank">Upgrade to pro</a> <button data-dismiss="true" class="dismiss-btn button button-link">I don’t want to save money</button>';
530 + $holiday_text = sprintf(
531 + '<p style="margin-top: 0; margin-bottom: 0px;"> 🎁 %s <strong>%s</strong> %s</p>',
532 + __('Get', 'templately'),
533 + __('Templately PRO', 'templately'),
534 + __('with up to 60% OFF & unlock 6500+ ready WordPress templates to power up web design in 2025.', 'templately')
535 + );
298 536
299 - $_black_friday = [
537 + $holiday_text .= "<div class='wpnotice-button-wrapper'>";
538 + $holiday_text .= sprintf(
539 + '<a class="button button-primary" target="_blank" href="%2$s">%1$s</a>',
540 + $crown . __('GET PRO Lifetime Access', 'templately'),
541 + Helper::web_url() . '#pricing'
542 + );
543 + $holiday_text .= sprintf(
544 + '<button class="button button-link dismiss-btn" data-dismiss="true">%1$s</button>',
545 + __('No, I\'ll Pay Full Price Later', 'templately')
546 + );
547 + $holiday_text .= "</div>";
548 +
549 +
550 + $_holiday = [
300 551 'thumbnail' => templately()->assets->icon( 'logos/logo-full.svg' ),
301 - 'html' => $notice_text,
552 + 'html' => $holiday_text,
302 553 ];
303 554
304 - $notices->add( 'black_friday', $_black_friday, [
305 - 'start' => $notices->time(),
306 - 'recurrence' => false,
307 - 'dismissible' => true,
308 - 'refresh' => TEMPLATELY_VERSION,
309 - "expire" => strtotime( '11:59:59pm 2nd December, 2023' ),
310 - ] );
555 + // $notices->add( 'holiday', $_holiday, [
556 + // 'start' => $notices->strtotime( '+0 minute' ),
557 + // 'recurrence' => false,
558 + // 'dismissible' => true,
559 + // 'refresh' => TEMPLATELY_VERSION,
560 + // "expire" => strtotime( '11:59:59pm 10th January, 2025' ),
561 + // 'display_if' => !wp_is_mobile(),
562 + // 'screens' => [
563 + // 'dashboard',
564 + // ],
565 + // ] );
311 566 }
312 567
313 568 self::$cache_bank->create_account( $notices );
314 569 self::$cache_bank->calculate_deposits( $notices );
@@ -321,16 +576,29 @@
321 576 */
322 577 public function admin_menu() {
323 578 // TODO: Role Management
324 579
325 - add_menu_page( 'Templately', 'Templately', 'delete_posts', 'templately', '', templately()->assets->icon( 'logos/logo-icon.svg' ), '58.7' );
580 + // 58.7 is the TOP-LEVEL slot: a float is correct and always has been
581 + // there, and the fractional part is what keeps Templately from
582 + // colliding with another plugin sitting at the same integer position.
583 + add_menu_page( 'Templately', 'Templately', 'delete_posts', 'templately', '', templately()->assets->icon( 'logos/logo-icon.svg' ), 58.7 );
326 584
585 + // The submenu calls pass NO position on purpose. `58.7` was copied down
586 + // from the top-level slot, where it never meant anything — a submenu
587 + // $position is an index within THIS menu, and 58.7 is past the end of a
588 + // four-item list, so every WordPress we support already just appended.
589 + // It was not merely useless though: before WP 6.0, $position had to be
590 + // an int (`is_int()`, not `is_numeric()` — a float fails it too), so
591 + // each call emitted `_doing_it_wrong` on every admin load of a
592 + // WP_DEBUG site inside the advertised floor (readme: WP 5.0) and then
593 + // discarded the value anyway. Omitting it appends — verified identical
594 + // ordering on WP 5.9 and 7.0 — and is silent on every version.
327 595 add_submenu_page( 'templately', 'Templately', 'Template Library', 'delete_posts', 'templately', [
328 596 $this,
329 597 'display'
330 - ], '58.7' );
598 + ] );
331 599
332 - add_submenu_page( 'templately', 'Theme Builder', 'Theme Builder', 'delete_posts', 'edit.php?post_type=templately_library', '', '58.7' );
600 + add_submenu_page( 'templately', 'Theme Builder', 'Theme Builder', Roles::CAP_BUILDER, 'edit.php?post_type=templately_library', '' );
333 601
334 602 // add_submenu_page( 'templately', 'Settings', 'Settings', 'administrator', 'templately-settings', [
335 603 // Settings::get_instance(),
336 604 // 'display'
@@ -337,12 +605,53 @@
337 605 // ], '58.7' );
338 606 }
339 607
340 608 public function display() {
341 - Helper::views( 'settings' );
609 + Helper::views( 'template-library', [
610 + 'show_grid_skeleton' => self::is_template_library_path(),
611 + ] );
342 612 }
343 613
344 614 /**
615 + * Whether the current `?path=` targets the template-library grid
616 + * (`TemplatesTypeLayout`, e.g. `elementor/packs`) — the only route the
617 + * pre-mount skeleton's card grid actually matches. Every other route
618 + * (`clouds/*`, `downloads`, `favourites`, `manage-sites`, `subscription`,
619 + * `purchased-items`, `ai-sites`, `settings/*`, item-detail pages, and
620 + * Saved Templates) gets a generic shell instead, so a hard reload doesn't
621 + * flash the wrong page shape before React mounts. See
622 + * views/template-library.php.
623 + *
624 + * Public + static (it was private on the pre-modular branch) purely so the
625 + * path parsing can be unit-tested on its own: it reads nothing but `$_GET`
626 + * and touches no instance state, so there is nothing for an Admin instance
627 + * to contribute, and constructing one would register every admin hook.
628 + *
629 + * @return bool
630 + */
631 + public static function is_template_library_path() {
632 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing hint for a skeleton; no state is changed.
633 + $path = isset( $_GET['path'] ) ? sanitize_text_field( wp_unslash( $_GET['path'] ) ) : '';
634 +
635 + if ( $path === '' ) {
636 + return true;
637 + }
638 +
639 + $segments = array_values( array_filter( explode( '/', trim( $path, '/' ) ) ) );
640 +
641 + $non_library_prefixes = [ 'clouds', 'downloads', 'favourites', 'manage-sites', 'subscription', 'purchased-items', 'ai-sites', 'settings' ];
642 +
643 + if ( in_array( $segments[0] ?? '', $non_library_prefixes, true ) ) {
644 + return false;
645 + }
646 +
647 + // `/:platform/:type` (the grid) vs `/:platform/:type/:slug` (item
648 + // detail, its own skeleton shape) or `/:platform/templates` (Saved
649 + // Templates, a table) — only the 2-segment form matches the grid.
650 + return count( $segments ) === 2 && $segments[1] !== 'templates';
651 + }
652 +
653 + /**
345 654 * If Elementor doesn't exists.
346 655 *
347 656 * @return void
348 657 */
@@ -347,12 +656,12 @@
347 656 * @return void
348 657 */
349 658 public static function has_no_elementor() {
350 659 $plugin_url = \wp_nonce_url( \self_admin_url( 'update.php?action=install-plugin&amp;plugin=elementor' ), 'install-plugin_elementor' );
351 - $button_text = 'Install Elementor';
660 + $button_text = __( 'Install Elementor', 'templately' );
352 661 if ( isset( Helper::get_plugins()['elementor/elementor.php'] ) ) {
353 662 $plugin_url = \wp_nonce_url( 'plugins.php?action=activate&amp;plugin=elementor/elementor.php', 'activate-plugin_elementor/elementor.php' );
354 - $button_text = 'Activate Elementor';
663 + $button_text = __( 'Activate Elementor', 'templately' );
355 664 }
356 665 $output = '<div class="notice notice-error">';
357 666 $output .= sprintf(
358 667 "<p><strong>%s</strong> %s <strong>%s</strong> %s &nbsp;&nbsp;<a class='button-primary' href='%s'>%s</a></p>",
@@ -360,14 +669,31 @@
360 669 __( 'requires', 'templately' ),
361 670 __( 'Elementor', 'templately' ),
362 671 __( 'plugin to be installed and activated. Please install Elementor to continue.', 'templately' ),
363 672 esc_url( $plugin_url ),
364 - __( $button_text, 'templately' )
673 + $button_text
365 674 );
366 675 $output .= '</div>';
367 - echo $output;
676 + echo wp_kses_post( $output );
368 677 }
369 678
370 679 public function header() {
371 680 Helper::views( 'header' );
681 + }
682 +
683 + /**
684 + * Handle links displayed below the plugin name in the WordPress Installed Plugins page.
685 + *
686 + * @return array
687 + * @since 3.1.2
688 + * @static
689 + *
690 + */
691 + public static function handleActionLinks($links, $file)
692 + {
693 + $settingsLink = '<a href="' . admin_url('admin.php?page=templately') . '" aria-label="' . __('Open settings page',
694 + 'templately') . '">' . __('Templates Library', 'templately') . '</a>';
695 +
696 + $links[] = $settingsLink;
697 + return $links;
372 698 }
373 699 }