diff --git a/CLAUDE.md b/CLAUDE.md
index 4510adec..6f6b8484 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,101 +4,124 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## What this is
-Sparkling — a free classic (non-block) WordPress theme by Colorlib, v2.6.0, derived from Underscores (_s) and built on **Bootstrap 3.4.1**. Text domain: `sparkling`. Verified against **WordPress 7.0.2 on PHP 8.5**.
+Sparkling — a free classic (non-block) **WordPress theme** by Colorlib, distributed on WordPress.org (slug `sparkling`) and licensed GPLv2+. It is a PHP theme derived from Underscores (`_s`), built on **Bootstrap 3.4.1** (CSS and JS) with Font Awesome 7.3.1 (a subset by default). `style.css` is hand-edited; there is **no SCSS pipeline**. Text domain: `sparkling`.
-This directory is a WordPress theme folder, not an application project: there is no `package.json`, `composer.json`, linter config, or test suite committed. Files ship exactly as they sit on disk.
+Do not migrate it to Bootstrap 5: users run plugins that emit Bootstrap 3 markup and rely on the theme's Bootstrap 3 (c8908ab reverted a Bootstrap 5 swap for that reason).
-## Working on the theme
+## Commands
-- **No build step.** `style.css` and `assets/css/*.css` are edited directly — there is no SCSS source. The `.min.css`/`.min.js` files are committed alongside their unminified twins and must be regenerated by hand (`npx terser in.js -c -m -o out.min.js`) when the source changes — nothing does it automatically.
-- **Which file is actually loaded matters.** `sparkling_scripts()` ([functions.php:311](functions.php#L311)) enqueues the *minified* `bootstrap.min.js` and `skip-link-focus-fix.min.js` but the *unminified* `functions.js`; the Customizer enqueues unminified `customizer.js`. Check the enqueue before editing a `.min` file.
-- **Line endings are CRLF** throughout. Keep them — mixing produces noisy diffs and a PHPCS warning.
-- **To run it**, copy or symlink this folder into a WordPress install's `wp-content/themes/`.
-- **Version** lives in the `style.css` theme header and is read once into the `SPARKLING_VERSION` constant ([functions.php:8](functions.php#L8)), which cache-busts every theme asset. Bump the header and every asset URL follows — never hardcode a version in an enqueue. The About screen's Changelog tab renders `changelog.txt` from the theme root.
+```bash
+npm ci # install the pinned JS/CSS toolchain
+composer install # install the pinned PHPCS, WPCS and PHPCompatibilityWP
-## Security model
+npm run build # produce sparkling.zip (runs i18n:check and version:check first)
+npm run verify # linters + regression tests + text domain + version check
+npm test # slider/menu on jQuery 3.7/4.0, contrast and package contents
+npm run lint # eslint + stylelint + php -l + phpcs, in parallel
+npm run lint:fix # auto-fix what eslint/stylelint/phpcbf can
+npm run i18n # regenerate languages/sparkling.pot (WP-CLI on PATH, or WP_CLI=/path/to/wp)
+npm run i18n:check # verify every gettext call uses the 'sparkling' text domain
+npm run version:check # style.css, readme.txt (Stable tag + changelog), changelog.txt, package.json agree
+npm run images # report optimisable PNG/JPEG; add -- --write to apply
-The theme was hardened in 2.5.0; keep these invariants when touching it.
+composer lint # vendor/bin/phpcs — the whole theme, zero errors and warnings
+composer fix # vendor/bin/phpcbf
+python3 .github/build-icon-subset.py . assets sparkling # rebuild the Font Awesome subset (needs fontTools)
+```
-- **Every AJAX handler needs a capability check *and* `check_ajax_referer()`.** Do not hook privileged work to `admin_init` — `admin-ajax.php` fires `admin_init` *before* its authentication branch, so an `admin_init` callback that acts on `$_GET` is reachable by anonymous requests. That was a real unauthenticated front-page takeover in 2.4.11.
-- **Colours printed into the inline `
- Header admin panel.
- *
- * @see sparkling_custom_header_setup().
- */
- function sparkling_admin_header_style() {
- ?>
-
- Header admin panel.
- *
- * @see sparkling_custom_header_setup().
- */
- function sparkling_admin_header_image() {
- $style = sprintf( ' style="color:#%s;"', esc_attr( get_header_textcolor() ) );
- ?>
-
';
+ $describe = ' aria-describedby="error-' . esc_attr( $field_id ) . '"';
+ }
+
+ $redirect = empty( $post->ID ) ? '' : '';
+
+ /*
+ * One line, with every inline element already inside a block, like core's
+ * own form: this is returned in place of the post content, so the_content's
+ * wpautop() runs over it and would otherwise add
and tags.
+ */
+ return '
';
}
+add_filter( 'the_password_form', 'custom_password_form', 10, 3 );
// Add Bootstrap classes for table
add_filter( 'the_content', 'sparkling_add_custom_table_class' );
@@ -92,25 +114,64 @@ function sparkling_add_custom_table_class( $content ) {
* @package sparkling
*/
function sparkling_social_icons() {
+ static $sparkling_instance = 0;
+
if ( has_nav_menu( 'social-menu' ) ) {
+ ++$sparkling_instance;
+
+ /*
+ * The menu can print several times on one page -- the footer and the
+ * Sparkling Social widget, in any number of widget areas -- and every
+ * copy used to carry id="menu-social". The first keeps that id for
+ * child themes that style it; the rest are numbered. The stylesheet
+ * matches the .menu-social class at the same specificity.
+ */
+ $sparkling_suffix = 1 === $sparkling_instance ? '' : '-' . $sparkling_instance;
+
wp_nav_menu(
array(
- 'theme_location' => 'social-menu',
- 'container' => 'nav',
- 'container_id' => 'menu-social',
- 'container_class' => 'social-icons',
- 'menu_id' => 'menu-social-items',
- 'menu_class' => 'social-menu',
- 'depth' => 1,
- 'fallback_cb' => '',
- 'link_before' => '',
- 'link_after' => '',
+ 'theme_location' => 'social-menu',
+ 'container' => 'nav',
+ 'container_id' => 'menu-social' . $sparkling_suffix,
+ 'container_class' => 'social-icons menu-social',
+ 'container_aria_label' => __( 'Social links', 'sparkling' ),
+ 'menu_id' => 'menu-social-items' . $sparkling_suffix,
+ 'menu_class' => 'social-menu',
+ 'depth' => 1,
+ 'fallback_cb' => '',
+ 'link_before' => '',
+ 'link_after' => '',
)
);
}
}
endif;
+if ( ! function_exists( 'sparkling_navwalker_is_icon_class' ) ) :
+ /**
+ * Whether a menu item's Title Attribute is an icon class list.
+ *
+ * The walker prints such a value as an icon before the label, which is how
+ * sites add Font Awesome or glyphicon icons to menu items. Anything else
+ * typed there is a tooltip.
+ *
+ * @param string $value Title Attribute.
+ *
+ * @return bool
+ */
+ function sparkling_navwalker_is_icon_class( $value ) {
+ $tokens = preg_split( '/\s+/', trim( (string) $value ) );
+
+ foreach ( $tokens as $token ) {
+ if ( ! preg_match( '/^(glyphicon(-[\w-]+)?|fa[srbl]?|fa-[\w-]+)$/', $token ) ) {
+ return false;
+ }
+ }
+
+ return '' !== $tokens[0];
+ }
+endif;
+
if ( ! function_exists( 'sparkling_header_menu' ) ) :
/**
* Header menu (should you choose to use one)
@@ -122,10 +183,11 @@ function sparkling_header_menu() {
'menu' => 'primary',
'theme_location' => 'primary',
'container' => 'div',
+ 'container_id' => 'sparkling-navbar',
'container_class' => 'collapse navbar-collapse navbar-ex1-collapse',
'menu_class' => 'nav navbar-nav',
- 'fallback_cb' => 'WP_Bootstrap_Navwalker::fallback',
- 'walker' => new WP_Bootstrap_Navwalker(),
+ 'fallback_cb' => 'Sparkling_Bootstrap_Navwalker::fallback',
+ 'walker' => new Sparkling_Bootstrap_Navwalker(),
)
);
} /* end header menu */
@@ -160,7 +222,7 @@ function sparkling_footer_links() {
* Call for action text and button displayed above content
*/
function sparkling_call_for_action() {
- if ( is_front_page() && of_get_option( 'w2f_cfa_text' ) != '' ) {
+ if ( is_front_page() && '' !== (string) of_get_option( 'w2f_cfa_text' ) ) {
echo '
';
echo '
';
echo '
';
@@ -180,7 +242,7 @@ function sparkling_call_for_action() {
* Featured image slider, displayed on front page for static page and blog
*/
function sparkling_featured_slider() {
- if ( is_front_page() && of_get_option( 'sparkling_slider_checkbox' ) == 1 ) {
+ if ( is_front_page() && of_get_option( 'sparkling_slider_checkbox' ) ) {
$count = absint( of_get_option( 'sparkling_slide_number' ) );
$slidecat = of_get_option( 'sparkling_slide_categories' );
@@ -217,7 +279,7 @@ function sparkling_featured_slider() {
// it and close the same tag we opened. Previously '' was
// printed unconditionally, leaving a stray closing tag whenever
// slide links were switched off.
- $sparkling_slide_linked = ( 1 == of_get_option( 'sparkling_slider_link_checkbox', 1 ) );
+ $sparkling_slide_linked = (bool) of_get_option( 'sparkling_slider_link_checkbox', 1 );
if ( $sparkling_slide_linked ) {
echo '
support questions and bug reports is via', 'sparkling' ); ?>
- .
-
-
-
-
-
-
-
-
-
-
-' . $css . '' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- See above.
+ }
}
+add_action( 'wp_head', 'get_sparkling_theme_options', 10 );
/**
* Add Bootstrap thumbnail styling to images with captions
@@ -539,19 +554,25 @@ function sparkling_caption( $output, $attr, $content ) {
$attr = shortcode_atts( $defaults, $attr );
+ $width = absint( $attr['width'] );
+
// If the width is less than 1 or there is no caption, return the content wrapped between the [caption] tags
- if ( $attr['width'] < 1 || empty( $attr['caption'] ) ) {
+ if ( $width < 1 || empty( $attr['caption'] ) ) {
return $content;
}
- // Set up the attributes for the caption
+ /*
+ * absint(): a non-numeric width -- "300px", say -- was added to 10 as a
+ * string, a fatal TypeError on PHP 8. The caption may hold the markup core
+ * allows in captions, and nothing else.
+ */
$attributes = ( ! empty( $attr['id'] ) ? ' id="' . esc_attr( $attr['id'] ) . '"' : '' );
$attributes .= ' class="thumbnail wp-caption ' . esc_attr( $attr['align'] ) . '"';
- $attributes .= ' style="width: ' . ( esc_attr( $attr['width'] ) + 10 ) . 'px"';
+ $attributes .= ' style="width: ' . ( $width + 10 ) . 'px"';
$output = '';
$output .= do_shortcode( $content );
- $output .= '' . $attr['caption'] . '';
+ $output .= '' . wp_kses_post( $attr['caption'] ) . '';
$output .= '';
return $output;
@@ -586,8 +607,8 @@ function sparkling_social() {
function sparkling_post_nav() {
the_post_navigation(
array(
- 'next_text' => '%title ',
- 'prev_text' => ' %title',
+ 'next_text' => '%title ',
+ 'prev_text' => ' %title',
'in_same_term' => true,
)
);
@@ -602,56 +623,28 @@ function sparkling_post_nav() {
function sparkling_paging_nav() {
the_posts_pagination(
array(
- 'prev_text' => ' ' . __( 'Newer posts', 'sparkling' ),
- 'next_text' => __( 'Older posts', 'sparkling' ) . ' ',
+ 'prev_text' => ' ' . __( 'Newer posts', 'sparkling' ),
+ 'next_text' => __( 'Older posts', 'sparkling' ) . ' ',
)
);
}
}
-/**
- * Adds the URL to the top level navigation menu item
- */
-function sparkling_add_top_level_menu_url( $atts, $item, $args ) {
- if ( ! wp_is_mobile() && isset( $args->has_children ) && $args->has_children ) {
- $atts['href'] = ! empty( $item->url ) ? $item->url : '';
- }
-
- return $atts;
-}
-
-add_filter( 'nav_menu_link_attributes', 'sparkling_add_top_level_menu_url', 99, 3 );
-
-/**
- * Makes the top level navigation menu item clickable
+/*
+ * sparkling_add_top_level_menu_url() and sparkling_make_top_level_menu_clickable()
+ * were removed in 2.7.0. The walker already prints every item's URL, and the
+ * footer script re-ran each top-level click through window.location -- with
+ * jQuery's deprecated .click(), only on desktop user agents (wp_is_mobile(),
+ * which also defeats page caching), and sending "#" items to "#".
*/
-function sparkling_make_top_level_menu_clickable() {
- if ( ! wp_is_mobile() ) {
- ?>
-
- ';
+ printf( '' . "\n", esc_url( get_bloginfo( 'pingback_url' ) ) );
}
}
-add_action( 'wp_head', '_s_pingback_header' );
+add_action( 'wp_head', 'sparkling_pingback_header' );
diff --git a/inc/fontawesome.php b/inc/fontawesome.php
new file mode 100644
index 00000000..db301faf
--- /dev/null
+++ b/inc/fontawesome.php
@@ -0,0 +1,255 @@
+ SPARKLING_VERSION,
+ 'full' => $full,
+ ),
+ true
+ );
+
+ return $full;
+}
+
+/**
+ * Scans the assigned nav menus for icons missing from the bundled subset.
+ *
+ * Only a class that is a real Font Awesome icon name counts. Sizing and style
+ * classes (`fa-fw`, `fa-2x`, `fa-spin`, `fa-solid`) are not icon names and are
+ * ignored, as is any unrelated class that happens to start with `fa-`.
+ *
+ * @return bool True if the complete library is needed.
+ */
+function sparkling_fontawesome_scan_menus() {
+ $classes = sparkling_fontawesome_menu_icon_classes();
+
+ if ( empty( $classes ) ) {
+ return false;
+ }
+
+ // Anything the subset already carries needs no further work.
+ $subset = sparkling_fontawesome_icon_names( array( 'subset/fontawesome-subset.min.css' ) );
+ $missing = array_diff( $classes, array_keys( $subset ) );
+
+ if ( empty( $missing ) ) {
+ return false;
+ }
+
+ // Only now is it worth reading the complete library to tell an icon name
+ // from a utility or unrelated class. all.min.css is the file the complete
+ // set loads, so it is also the authority on what that set can draw.
+ $complete = sparkling_fontawesome_icon_names( array( 'all.min.css' ) );
+
+ foreach ( $missing as $class ) {
+ if ( isset( $complete[ $class ] ) ) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Collects every `fa-` class used in the assigned nav menus.
+ *
+ * Reads the Navigation Label, the Title Attribute and the CSS Classes field,
+ * the three places a menu item can carry an icon.
+ *
+ * @return array List of lowercased class names.
+ */
+function sparkling_fontawesome_menu_icon_classes() {
+ $locations = get_nav_menu_locations();
+ $found = array();
+
+ foreach ( array_keys( get_registered_nav_menus() ) as $location ) {
+ if ( empty( $locations[ $location ] ) ) {
+ continue;
+ }
+
+ $items = wp_get_nav_menu_items( $locations[ $location ] );
+
+ if ( ! is_array( $items ) ) {
+ continue;
+ }
+
+ foreach ( $items as $item ) {
+ $haystack = isset( $item->title ) ? $item->title : '';
+
+ // The primary menu's walker prints an icon class list typed into the
+ // Title Attribute field as an icon, so it counts too.
+ if ( ! empty( $item->attr_title ) ) {
+ $haystack .= ' ' . $item->attr_title;
+ }
+
+ if ( ! empty( $item->classes ) && is_array( $item->classes ) ) {
+ $haystack .= ' ' . implode( ' ', $item->classes );
+ }
+
+ if ( preg_match_all( '/\bfa-[a-z0-9]+(?:-[a-z0-9]+)*/', strtolower( $haystack ), $matches ) ) {
+ $found = array_merge( $found, $matches[0] );
+ }
+ }
+ }
+
+ return array_values( array_unique( $found ) );
+}
+
+/**
+ * Reads icon names out of the bundled Font Awesome stylesheets.
+ *
+ * An icon is a rule that declares the `--fa` custom property holding its
+ * glyph; utility classes such as `.fa-fw` declare other properties and are
+ * skipped. Selectors are read as a group so aliases -- `.fa-edit` alongside
+ * `.fa-pen-to-square` -- are all collected, and the version 4 compatibility
+ * layer's `.fa.fa-glass` form counts too, since the complete set draws it.
+ *
+ * @param array $files Stylesheet paths relative to the Font Awesome directory.
+ * @return array Map of icon name to true, for O(1) lookup.
+ */
+function sparkling_fontawesome_icon_names( $files ) {
+ $names = array();
+
+ foreach ( $files as $file ) {
+ $path = sparkling_fontawesome_dir() . $file;
+
+ if ( ! is_readable( $path ) ) {
+ continue;
+ }
+
+ // A theme-owned file on local disk, read once and cached thereafter.
+ $css = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+
+ if ( ! $css ) {
+ continue;
+ }
+
+ $css = preg_replace( '#/\*.*?\*/#s', '', $css );
+
+ if ( ! preg_match_all( '/([^{}]+)\{([^{}]*)\}/', $css, $rules, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $rules as $rule ) {
+ if ( ! preg_match( '/--fa\s*:/', $rule[2] ) ) {
+ continue;
+ }
+
+ foreach ( explode( ',', $rule[1] ) as $selector ) {
+ if ( preg_match( '/^\s*(?:\.fa)?\.(fa-[a-z0-9-]+)\s*$/', $selector, $name ) ) {
+ $names[ $name[1] ] = true;
+ }
+ }
+ }
+ }
+
+ return $names;
+}
+
+/**
+ * Drops the cached scan so the next front-end request recomputes it.
+ */
+function sparkling_fontawesome_clear_cache() {
+ delete_option( SPARKLING_FONTAWESOME_CACHE );
+}
+
+add_action( 'wp_update_nav_menu', 'sparkling_fontawesome_clear_cache' );
+add_action( 'wp_delete_nav_menu', 'sparkling_fontawesome_clear_cache' );
+add_action( 'customize_save_after', 'sparkling_fontawesome_clear_cache' );
+add_action( 'after_switch_theme', 'sparkling_fontawesome_clear_cache' );
diff --git a/inc/jetpack.php b/inc/jetpack.php
index 5730d465..fde16fb2 100644
--- a/inc/jetpack.php
+++ b/inc/jetpack.php
@@ -12,7 +12,8 @@
*/
function sparkling_jetpack_setup() {
add_theme_support(
- 'infinite-scroll', array(
+ 'infinite-scroll',
+ array(
'type' => 'click',
'container' => 'main',
'footer' => 'page',
diff --git a/inc/metaboxes.php b/inc/metaboxes.php
index 2c514a6b..6549d4c4 100644
--- a/inc/metaboxes.php
+++ b/inc/metaboxes.php
@@ -11,25 +11,17 @@
* Add Meta box in page and post post types.
*/
function sparkling_add_custom_box() {
- add_meta_box(
- 'siderbar-layout', //Unique ID
- __( 'Select layout for this specific Page only ( Note: This setting only reflects if page Template is set as Default Template and Blog Type Templates.)', 'sparkling' ), //Title
- 'sparkling_sidebar_layout', //Callback function
- 'page' //show metabox in pages
- );
- add_meta_box(
- 'siderbar-layout', //Unique ID
- __( 'Select layout for this specific Post only', 'sparkling' ), //Title
- 'sparkling_sidebar_layout', //Callback function
- 'post', //show metabox in posts
- 'side'
- );
- if ( class_exists( 'WooCommerce' ) ) {
+ foreach ( array( 'page', 'post', 'product' ) as $sparkling_post_type ) {
+ if ( 'product' === $sparkling_post_type && ! class_exists( 'WooCommerce' ) ) {
+ continue;
+ }
+
+ // The ids are unchanged so the boxes keep their saved position and state.
add_meta_box(
- 'product-siderbar-layout', //Unique ID
- __( 'Select layout for this specific Product only', 'sparkling' ), //Title
- 'sparkling_sidebar_layout', //Callback function
- 'product', //show metabox in posts
+ 'product' === $sparkling_post_type ? 'product-siderbar-layout' : 'siderbar-layout',
+ __( 'Layout', 'sparkling' ),
+ 'sparkling_sidebar_layout',
+ $sparkling_post_type,
'side'
);
}
@@ -56,27 +48,19 @@ function sparkling_sidebar_layout( $post ) {
// Use nonce for verification
wp_nonce_field( basename( __FILE__ ), 'custom_meta_box_nonce' ); ?>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
%2$s';
- /*
- if ( get_the_time( 'U' ) !== get_the_modified_time( 'U' ) ) {
- $time_string .= '';
- }
- */
-
$time_string = sprintf(
$time_string,
esc_attr( get_the_date( 'c' ) ),
@@ -49,19 +43,25 @@ function sparkling_posted_on() {
esc_html( get_the_modified_date() )
);
+ printf(
+ '%2$s',
+ esc_url( get_permalink() ),
+ $time_string // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Each part escaped above.
+ );
+
+ /*
+ * A post whose author account was deleted, or an attachment uploaded
+ * by no one, has no author: this printed an empty link to /author/.
+ */
+ $author_name = get_the_author();
+
+ if ( '' !== (string) $author_name ) {
printf(
- ' %1$s %2$s',
- sprintf(
- '%2$s',
- esc_url( get_permalink() ),
- $time_string
- ),
- sprintf(
- '%2$s',
- esc_url( get_author_posts_url( get_the_author_meta( 'ID' ) ) ),
- esc_html( get_the_author() )
- )
+ '%2$s',
+ esc_url( get_author_posts_url( get_the_author_meta( 'ID' ) ) ),
+ esc_html( $author_name )
);
+ }
}
endif;
diff --git a/inc/welcome-screen/class-sparkling-notify-system.php b/inc/welcome-screen/class-sparkling-notify-system.php
deleted file mode 100644
index a74d16c0..00000000
--- a/inc/welcome-screen/class-sparkling-notify-system.php
+++ /dev/null
@@ -1,291 +0,0 @@
-=' );
- }
-
- /**
- * @return bool
- */
- public static function is_not_static_page() {
- return 'page' == get_option( 'show_on_front' ) ? true : false;
- }
-
- /**
- * @return bool
- */
- public static function has_widgets() {
- if ( ! is_active_sidebar( 'primary-widgets' ) && ! is_active_sidebar( 'secondary-widgets' ) ) {
- return false;
- }
-
- return true;
- }
-
- /**
- * @return bool
- */
- public static function has_posts() {
-
- $imported = get_option( 'sparkling_importded_demo' );
- if ( $imported ) {
- return true;
- }
-
- $count_posts = wp_count_posts( 'post' );
- if ( $count_posts->publish > 4 ) {
- add_option( 'sparkling_importded_demo', true );
- return true;
- }
-
- $args = array(
- 's' => 'Beautiful Flat Design and Long Shadow Logo Collection',
- );
- $query = get_posts( $args );
- if ( ! empty( $query ) ) {
- add_option( 'sparkling_importded_demo', true );
- return true;
- }
-
- return false;
- }
-
- /**
- * @return bool
- */
- public static function has_content() {
- $check = array(
- 'widgets' => self::has_widgets(),
- 'posts' => self::has_posts(),
- );
-
- if ( $check['widgets'] && $check['posts'] ) {
- return true;
- }
-
- return false;
- }
-
- /**
- * @return bool
- */
- public static function check_wordpress_importer() {
- if ( file_exists( ABSPATH . 'wp-content/plugins/wordpress-importer/wordpress-importer.php' ) ) {
- include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
-
- return is_plugin_active( 'wordpress-importer/wordpress-importer.php' );
- }
-
- return false;
- }
-
- /**
- * @return bool
- */
- public static function check_plugin_is_installed( $slug ) {
- if ( file_exists( ABSPATH . 'wp-content/plugins/' . $slug . '/' . $slug . '.php' ) ) {
- return true;
- }
-
- return false;
- }
-
- /**
- * @return bool
- */
- public static function check_plugin_is_active( $slug ) {
- if ( file_exists( ABSPATH . 'wp-content/plugins/' . $slug . '/' . $slug . '.php' ) ) {
- include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
-
- return is_plugin_active( $slug . '/' . $slug . '.php' );
- }
- }
-
- public static function has_plugin( $slug = null ) {
-
- $check = array(
- 'installed' => self::check_plugin_is_installed( $slug ),
- 'active' => self::check_plugin_is_active( $slug ),
- );
-
- if ( ! $check['installed'] || ! $check['active'] ) {
- return false;
- }
-
- return true;
-
- }
-
- public static function has_import_plugin( $slug = null ) {
- $return = self::has_content();
-
- if ( $return ) {
- return true;
- }
- $check = array(
- 'installed' => self::check_plugin_is_installed( $slug ),
- 'active' => self::check_plugin_is_active( $slug ),
- );
-
- if ( ! $check['installed'] || ! $check['active'] ) {
- return false;
- }
-
- return true;
- }
-
- public static function has_import_content() {
-
- $plugin = self::has_import_plugin( 'wordpress-importer' );
- $content = self::has_posts();
- if ( ! $plugin || ! $content ) {
- return false;
- }
-
- return true;
-
- }
-
- public static function has_import_plugins() {
- $check = array(
- 'wordpress-importer' => array(
- 'installed' => false,
- 'active' => false,
- ),
- 'widget-importer-exporter' => array(
- 'installed' => false,
- 'active' => false,
- ),
- );
-
- $content = self::has_content();
- $return = false;
- if ( $content ) {
- return true;
- }
-
- $stop = false;
- foreach ( $check as $plugin => $val ) {
- if ( $stop ) {
- continue;
- }
-
- $check[ $plugin ]['installed'] = self::check_plugin_is_installed( $plugin );
- $check[ $plugin ]['active'] = self::check_plugin_is_active( $plugin );
-
- if ( ! $check[ $plugin ]['installed'] || ! $check[ $plugin ]['active'] ) {
- $return = true;
- $stop = true;
- }
- }
-
- return $return;
- }
-
- public static function widget_importer_exporter_title() {
- $installed = self::check_plugin_is_installed( 'widget-importer-exporter' );
- if ( ! $installed ) {
- return __( 'Install: Widget Importer Exporter Plugin', 'sparkling' );
- }
-
- $active = self::check_plugin_is_active( 'widget-importer-exporter' );
- if ( $installed && ! $active ) {
- return __( 'Activate: Widget Importer Exporter Plugin', 'sparkling' );
- }
-
- return __( 'Install: Widget Importer Exporter Plugin', 'sparkling' );
- }
-
- public static function wordpress_importer_title() {
- $installed = self::check_plugin_is_installed( 'wordpress-importer' );
- if ( ! $installed ) {
- return __( 'Install: WordPress Importer', 'sparkling' );
- }
-
- $active = self::check_plugin_is_active( 'wordpress-importer' );
- if ( $installed && ! $active ) {
- return __( 'Activate: WordPress Importer', 'sparkling' );
- }
-
- return __( 'Install: WordPress Importer', 'sparkling' );
- }
-
- /**
- * @return string
- */
- public static function wordpress_importer_description() {
- $installed = self::check_plugin_is_installed( 'wordpress-importer' );
- if ( ! $installed ) {
- return __( 'Please install the WordPress Importer to create the demo content.', 'sparkling' );
- }
-
- $active = self::check_plugin_is_active( 'wordpress-importer' );
- if ( $installed && ! $active ) {
- return __( 'Please activate the WordPress Importer to create the demo content.', 'sparkling' );
- }
-
- return __( 'Please install the WordPress Importer to create the demo content.', 'sparkling' );
- }
-
- public static function widget_importer_exporter_description() {
- $installed = self::check_plugin_is_installed( 'widget-importer-exporter' );
- if ( ! $installed ) {
- return __( 'Please install the WordPress widget importer to create the demo content', 'sparkling' );
- }
-
- $active = self::check_plugin_is_active( 'widget-importer-exporter' );
- if ( $installed && ! $active ) {
- return __( 'Please activate the WordPress Widget Importer to create the demo content.', 'sparkling' );
- }
-
- return __( 'Please install the WordPress widget importer to create the demo content', 'sparkling' );
-
- }
-
- public static function create_plugin_requirement_title( $install_text, $activate_text, $plugin_slug ) {
-
- if ( '' == $plugin_slug ) {
- return;
- }
- if ( '' == $install_text && '' == $activate_text ) {
- return;
- }
- if ( '' == $install_text && '' == $activate_text ) {
- $install_text = $activate_text;
- } elseif ( '' == $activate_text && '' == $install_text ) {
- $activate_text = $install_text;
- }
-
- $installed = self::check_plugin_is_installed( $plugin_slug );
- if ( ! $installed ) {
- return $install_text;
- } elseif ( ! self::check_plugin_is_active( $plugin_slug ) && $installed ) {
- return $activate_text;
- } else {
- return '';
- }
-
- }
-
- /**
- * @return bool
- */
- public static function is_not_template_front_page() {
- $page_id = get_option( 'page_on_front' );
-
- return get_page_template_slug( $page_id ) == 'page-templates/frontpage-template.php' ? true : false;
- }
- }
-}// End if().
diff --git a/inc/welcome-screen/class-sparkling-welcome.php b/inc/welcome-screen/class-sparkling-welcome.php
index 5fc9bc34..867f1574 100644
--- a/inc/welcome-screen/class-sparkling-welcome.php
+++ b/inc/welcome-screen/class-sparkling-welcome.php
@@ -1,562 +1,361 @@
-id ) {
- return $classes;
- }
-
- $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'getting_started';
-
- if ( in_array( $tab, array( 'recommended_plugins', 'recommended_actions' ), true ) ) {
- $classes .= ' plugin-install-php';
- }
-
- return $classes;
- }
-
- /**
- * Look up a published page by title without the deprecated get_page_by_title().
- *
- * @param string $title Page title to match.
- * @return WP_Post|null
- */
- private function get_page_by_title( $title ) {
- $query = new WP_Query(
- array(
- 'post_type' => 'page',
- 'title' => $title,
- 'post_status' => 'publish',
- 'posts_per_page' => 1,
- 'no_found_rows' => true,
- 'ignore_sticky_posts' => true,
- 'update_post_term_cache' => false,
- 'update_post_meta_cache' => false,
- )
- );
-
- return empty( $query->posts ) ? null : $query->posts[0];
- }
-
- /**
- * AJAX: point the front page at the "Homepage" page and the blog at "Blog".
- *
- * Only ever runs for an authenticated user who can manage options and who
- * presents a valid nonce.
- */
- public function sparkling_set_pages() {
- if ( ! current_user_can( 'manage_options' ) ) {
- wp_send_json_error( esc_html__( 'You are not allowed to do that.', 'sparkling' ), 403 );
- }
-
- check_ajax_referer( 'sparkling_welcome_nonce', 'nonce' );
-
- $about = $this->get_page_by_title( 'Homepage' );
- $blog = $this->get_page_by_title( 'Blog' );
-
- if ( ! $about instanceof WP_Post ) {
- wp_send_json_error( esc_html__( 'No page titled "Homepage" was found.', 'sparkling' ), 404 );
- }
-
- update_option( 'page_on_front', $about->ID );
- update_option( 'show_on_front', 'page' );
-
- if ( $blog instanceof WP_Post ) {
- update_option( 'page_for_posts', $blog->ID );
- }
-
- wp_send_json_success( 'success' );
- }
-
- /**
- * Note: sparkling_activate_plugin() and sparkling_deactivate_plugin() were removed.
- *
- * They were hooked to admin_init and acted on ?action=activate_plugin /
- * ?action=deactivate_plugin without verifying a nonce, which made them
- * CSRF-exploitable against any administrator. Nothing in the theme ever
- * generated those links: the recommended-plugins tab builds its buttons with
- * create_action_link(), which points at core's plugins.php/update.php using
- * core's own nonces. The methods were therefore unreachable dead code and
- * pure attack surface, so they have been deleted rather than patched.
- */
-
- /**
- * Creates the dashboard page
- *
- * @see add_theme_page()
- * @since 1.8.2.4
- */
- public function sparkling_welcome_register_menu() {
- $action_count = $this->count_actions();
- $title = $action_count > 0 ? 'About Sparkling ' . esc_html( $action_count ) . '' : 'About Sparkling';
-
- add_theme_page(
- 'About Sparkling', $title, 'edit_theme_options', 'sparkling-welcome', array(
- $this,
- 'sparkling_welcome_screen',
- )
- );
- }
-
- /**
- * Adds an admin notice upon successful activation.
- *
- * @since 1.8.2.4
- */
- public function sparkling_activation_admin_notice() {
- global $pagenow;
-
- if ( is_admin() && ( 'themes.php' == $pagenow ) && isset( $_GET['activated'] ) ) {
- add_action( 'admin_notices', array( $this, 'sparkling_welcome_admin_notice' ), 99 );
- }
- }
-
- /**
- * Display an admin notice linking to the welcome screen
- *
- * @since 1.8.2.4
- */
- public function sparkling_welcome_admin_notice() {
- ?>
-
-
', '' ); ?>
-
-
- base ) {
- return;
- }
-
- wp_enqueue_style( 'sparkling-welcome-screen-css', get_template_directory_uri() . '/inc/welcome-screen/css/welcome.css', array(), SPARKLING_VERSION );
-
- /*
- * The recommended-plugins tab renders core's plugin-card markup. Enqueue
- * core's assets here rather than inside the section template, which runs
- * after the head has been printed and so only landed via late styles.
- */
- if ( $is_welcome_page ) {
- wp_enqueue_style( 'plugin-install' );
- wp_enqueue_script( 'plugin-install' );
- wp_enqueue_script( 'updates' );
- add_thickbox();
- }
-
- if ( 'customize' != $screen->base ) {
- wp_enqueue_script( 'sparkling-welcome-screen-js', get_template_directory_uri() . '/inc/welcome-screen/js/welcome.js', array( 'jquery' ), '1.0', true );
-
- wp_localize_script(
- 'sparkling-welcome-screen-js', 'sparklingWelcomeScreenObject', array(
- 'nr_actions_required' => $this->count_actions(),
- 'ajaxurl' => admin_url( 'admin-ajax.php' ),
- 'template_directory' => get_template_directory_uri(),
- 'nonce' => wp_create_nonce( 'sparkling_welcome_nonce' ),
- 'no_required_actions_text' => __( 'Hooray! There are no required actions for you right now.', 'sparkling' ),
- )
- );
- }
-
- }
-
- /**
- * Dismiss required actions
- *
- * @since 1.8.2.4
- */
- public function sparkling_dismiss_required_action_callback() {
- global $sparkling_required_actions;
-
- if ( ! current_user_can( 'edit_theme_options' ) ) {
- wp_send_json_error( esc_html__( 'You are not allowed to do that.', 'sparkling' ), 403 );
- }
-
- check_ajax_referer( 'sparkling_welcome_nonce', 'nonce' );
-
- $action_id = isset( $_GET['id'] ) ? sanitize_key( wp_unslash( $_GET['id'] ) ) : 0;
- $todo = isset( $_GET['todo'] ) ? sanitize_key( wp_unslash( $_GET['todo'] ) ) : '';
- echo esc_html( $action_id ); /* this is needed and it's the id of the dismissable required action */
- if ( ! empty( $action_id ) ) :
- /* if the option exists, update the record for the specified id */
- if ( get_option( 'sparkling_show_required_actions' ) ) :
- $sparkling_show_required_actions = get_option( 'sparkling_show_required_actions' );
- switch ( $todo ) {
- case 'add':
- $sparkling_show_required_actions[ $action_id ] = true;
- break;
- case 'dismiss':
- $sparkling_show_required_actions[ $action_id ] = false;
- break;
- }
- update_option( 'sparkling_show_required_actions', $sparkling_show_required_actions );
- /* create the new option,with false for the specified id */
- else :
- $sparkling_show_required_actions_new = array();
- if ( ! empty( $sparkling_required_actions ) ) :
- foreach ( $sparkling_required_actions as $sparkling_required_action ) :
- if ( $sparkling_required_action['id'] == $action_id ) :
- $sparkling_show_required_actions_new[ $sparkling_required_action['id'] ] = false;
- else :
- $sparkling_show_required_actions_new[ $sparkling_required_action['id'] ] = true;
- endif;
- endforeach;
- update_option( 'sparkling_show_required_actions', $sparkling_show_required_actions_new );
- endif;
- endif;
- endif;
- die(); // this is required to return a proper result
- }
-
- public function sparkling_dismiss_recommended_plugins_callback() {
- if ( ! current_user_can( 'edit_theme_options' ) ) {
- wp_send_json_error( esc_html__( 'You are not allowed to do that.', 'sparkling' ), 403 );
- }
-
- check_ajax_referer( 'sparkling_welcome_nonce', 'nonce' );
-
- $action_id = isset( $_GET['id'] ) ? sanitize_key( wp_unslash( $_GET['id'] ) ) : 0;
- $todo = isset( $_GET['todo'] ) ? sanitize_key( wp_unslash( $_GET['todo'] ) ) : '';
- echo esc_html( $action_id ); /* this is needed and it's the id of the dismissable required action */
- if ( ! empty( $action_id ) ) :
- /* if the option exists, update the record for the specified id */
- $sparkling_show_recommended_plugins = get_option( 'sparkling_show_recommended_plugins' );
-
- if ( ! is_array( $sparkling_show_recommended_plugins ) ) {
- $sparkling_show_recommended_plugins = array();
- }
-
- switch ( $todo ) {
- case 'add':
- $sparkling_show_recommended_plugins[ $action_id ] = false;
- break;
- case 'dismiss':
- $sparkling_show_recommended_plugins[ $action_id ] = true;
- break;
- }
- update_option( 'sparkling_show_recommended_plugins', $sparkling_show_recommended_plugins );
- /* create the new option,with false for the specified id */
- endif;
- die(); // this is required to return a proper result
- }
-
- /**
- * Count the number of required actions
- */
- public function count_actions() {
- global $sparkling_required_actions;
-
- $sparkling_show_required_actions = get_option( 'sparkling_show_required_actions' );
- if ( ! is_array( $sparkling_show_required_actions ) ) {
- $sparkling_show_required_actions = array();
- }
-
- if ( ! is_array( $sparkling_required_actions ) ) {
- return 0;
- }
-
- $i = 0;
- foreach ( $sparkling_required_actions as $action ) {
- $true = false;
-
- if ( ! isset( $action['id'] ) ) {
- continue;
- }
-
- if ( empty( $action['check'] ) ) {
- $true = true;
- }
-
- if ( ! empty( $sparkling_show_required_actions ) && isset( $sparkling_show_required_actions[ $action['id'] ] ) && ! $sparkling_show_required_actions[ $action['id'] ] ) {
- $true = false;
- }
-
- if ( $true ) {
- $i ++;
- }
- }
-
- return $i;
- }
-
- /**
- * Call plugin API to get plugins info
- */
- public function call_plugin_api( $slug ) {
- include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' );
- $call_api = get_transient( 'sparkling_plugin_information_transient_' . $slug );
- if ( false === $call_api ) {
- $call_api = plugins_api(
- 'plugin_information', array(
- 'slug' => $slug,
- 'fields' => array(
- 'downloaded' => false,
- 'rating' => false,
- 'description' => false,
- 'short_description' => true,
- 'donate_link' => false,
- 'tags' => false,
- 'sections' => true,
- 'homepage' => true,
- 'added' => false,
- 'last_updated' => false,
- 'compatibility' => false,
- 'tested' => false,
- 'requires' => false,
- 'downloadlink' => false,
- 'icons' => true,
- ),
- )
- );
- set_transient( 'sparkling_plugin_information_transient_' . $slug, $call_api, 30 * MINUTE_IN_SECONDS );
- }
-
- return $call_api;
- }
-
- /**
- * Check if a plugin is active
- */
- public function check_active( $slug ) {
- if ( file_exists( ABSPATH . 'wp-content/plugins/' . $slug . '/' . $slug . '.php' ) ) {
- include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
-
- $needs = is_plugin_active( $slug . '/' . $slug . '.php' ) ? 'deactivate' : 'activate';
-
- return array(
- 'status' => is_plugin_active( $slug . '/' . $slug . '.php' ),
- 'needs' => $needs,
- );
- }
-
- return array(
- 'status' => false,
- 'needs' => 'install',
- );
- }
-
- /**
- * Check for icon
- */
- public function check_for_icon( $arr ) {
- if ( ! empty( $arr['svg'] ) ) {
- $plugin_icon_url = $arr['svg'];
- } elseif ( ! empty( $arr['2x'] ) ) {
- $plugin_icon_url = $arr['2x'];
- } elseif ( ! empty( $arr['1x'] ) ) {
- $plugin_icon_url = $arr['1x'];
- } else {
- $plugin_icon_url = $arr['default'];
- }
-
- return $plugin_icon_url;
- }
-
- /**
- * Create action link
- */
- public function create_action_link( $state, $slug ) {
- switch ( $state ) {
- case 'install':
- return wp_nonce_url(
- add_query_arg(
- array(
- 'action' => 'install-plugin',
- 'plugin' => $slug,
- ),
- network_admin_url( 'update.php' )
- ),
- 'install-plugin_' . $slug
- );
- break;
- case 'deactivate':
- return add_query_arg(
- array(
- 'action' => 'deactivate',
- 'plugin' => rawurlencode( $slug . '/' . $slug . '.php' ),
- 'plugin_status' => 'all',
- 'paged' => '1',
- '_wpnonce' => wp_create_nonce( 'deactivate-plugin_' . $slug . '/' . $slug . '.php' ),
- ), network_admin_url( 'plugins.php' )
- );
- break;
- case 'activate':
- return add_query_arg(
- array(
- 'action' => 'activate',
- 'plugin' => rawurlencode( $slug . '/' . $slug . '.php' ),
- 'plugin_status' => 'all',
- 'paged' => '1',
- '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $slug . '/' . $slug . '.php' ),
- ), network_admin_url( 'plugins.php' )
- );
- break;
- }
- }
-
- /**
- * Welcome screen content
- *
- * @since 1.8.2.4
- */
- public function sparkling_welcome_screen() {
- if ( ! current_user_can( 'edit_theme_options' ) ) {
- wp_die( esc_html__( 'You are not allowed to access this page.', 'sparkling' ) );
- }
-
- $sparkling = wp_get_theme( get_template() ); // the parent: a child theme would report its own version
- $allowed = array( 'getting_started', 'recommended_actions', 'recommended_plugins', 'changelog', 'support' );
- $active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'getting_started';
-
- if ( ! in_array( $active_tab, $allowed, true ) ) {
- $active_tab = 'getting_started';
- }
-
- ?>
-
-
' . esc_html__( 'Hooray! There are no required actions for you right now. But you can make your theme more powerful with next actions: ', 'sparkling' ) . '
' . esc_html__( 'The changelog file could not be found.', 'sparkling' ) . '
';
} else {
- WP_Filesystem();
- global $wp_filesystem;
-
- $sparkling_changelog = $wp_filesystem ? $wp_filesystem->get_contents( $sparkling_changelog_file ) : false;
+ /*
+ * A plain read of the theme's own file. WP_Filesystem() is for writing
+ * during upgrades: on a host without direct file access it needs FTP
+ * credentials, so the tab showed "could not be read".
+ */
+ $sparkling_changelog = file_get_contents( $sparkling_changelog_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local theme file.
if ( false === $sparkling_changelog || '' === $sparkling_changelog ) {
echo '
' . esc_html__( 'The changelog file could not be read.', 'sparkling' ) . '
+
diff --git a/phpcs.ruleset.xml b/phpcs.ruleset.xml
deleted file mode 100644
index 90a2ad7e..00000000
--- a/phpcs.ruleset.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
- A custom set of code standard rules to check for WordPress themes and plugins.
-
-
-
-
- */node_modules/*
- */woocommerce/*
-
-
-
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 00000000..5f94a874
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,39 @@
+
+
+ Coding standard for the Sparkling theme: WordPress-Extra -- formatting, naming and the escaping, nonce and sanitising checks WordPress.org theme review applies -- plus PHP 7.4+ compatibility.
+
+
+ .
+ /node_modules/*
+ /vendor/*
+ /build/*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/readme.md b/readme.md
index 9346630c..14e9e579 100644
--- a/readme.md
+++ b/readme.md
@@ -1,7 +1,7 @@
# Sparkling — Free Responsive WordPress Blog & Business Theme
-[](https://github.com/ColorlibHQ/Sparkling/releases)
-[](https://wordpress.org/themes/sparkling/)
+[](https://github.com/ColorlibHQ/Sparkling/releases)
+[](https://wordpress.org/themes/sparkling/)
[](https://www.php.net/)
[](https://www.gnu.org/licenses/gpl-2.0.html)
[](https://wordpress.org/themes/sparkling/)
@@ -33,7 +33,7 @@ four sidebar layouts, dozens of live Customizer options, and translations in ove
| | |
|---|---|
-| WordPress | 6.0 or newer (tested to 7.0) |
+| WordPress | 6.0 or newer (tested to 7.1) |
| PHP | 7.4 or newer (tested to 8.5) |
| Browsers | All current evergreen browsers. Internet Explorer is no longer supported. |
@@ -65,8 +65,11 @@ for a guided setup.
- **Translation ready**, WPML compatible, with 20+ bundled translations
- **WooCommerce ready**
- **No jQuery dependency** in the theme's own JavaScript
-- **Block editor support** — responsive embeds, block styles, wide blocks and an editor stylesheet
-- **Accessibility touches** — skip link, visible keyboard focus, `prefers-reduced-motion` support
+- **Block editor support** — a theme.json palette that follows your Customizer colours, an editor sized
+ like the column the post renders in, four block patterns and a "Blocks (full width, no title)" page template
+- **Accessibility** — WCAG AA text contrast, labelled landmarks, one h1 per page, keyboard-operable
+ submenus and Customizer switches, skip link, `prefers-reduced-motion` support
+- **jQuery 4 ready** — works on jQuery 3.7 and on jQuery 4.0 without Migrate
## Customizer options
@@ -74,7 +77,7 @@ All settings live under **Appearance → Customize → Sparkling Options**:
| Section | What it controls |
|---|---|
-| Content | Post excerpts, comments on static pages |
+| Content | Post excerpts, comments on static pages, which Font Awesome library to load |
| Slider | On/off, source category, number of slides, slide links |
| Layout | Sidebar position, element colours, WooCommerce page layout |
| Call to action | Text, button label, link, colours |
@@ -121,14 +124,22 @@ The theme ships ready to run; there is no build step required to use it.
```bash
git clone https://github.com/ColorlibHQ/Sparkling.git
# symlink or copy into wp-content/themes/sparkling
+npm ci && composer install # the pinned lint and build toolchain
```
-`style.css` and the files in `assets/css/` are edited directly. Minified assets are committed alongside
-their sources and must be regenerated by hand when the source changes:
+`style.css` and the files in `assets/css/` are edited directly. The scripts:
-```bash
-npx terser assets/js/widget.js -c -m -o assets/js/widget.min.js
-```
+| Command | What it does |
+|---|---|
+| `npm run verify` | Linters at zero warnings, frontend and package regression tests, text domains and version checks |
+| `npm test` | Slider and menu regression tests on jQuery 3.7/4.0, default contrast and package contents |
+| `npm run build` | `sparkling.zip`, containing only runtime directories and known theme files |
+| `npm run i18n` | regenerates `languages/sparkling.pot` (needs WP-CLI) |
+| `npm run images` | reports, or with `-- --write` applies, lossless image optimisation |
+
+GitHub Actions runs the same checks on PHP 7.4–8.5 for every push and pull request.
+Development uses Node.js 22.22.2+, 24.15+ or 26+, Composer and Python 3. Python reads the ZIP in the package regression test; fontTools is needed only when rebuilding the icon subset. The theme itself needs none of these tools.
+[`CLAUDE.md`](CLAUDE.md) documents the architecture and the gotchas.
Bug reports and pull requests are welcome on the [issue tracker](https://github.com/ColorlibHQ/Sparkling/issues).
diff --git a/readme.txt b/readme.txt
index aa1838fd..0438c7bc 100644
--- a/readme.txt
+++ b/readme.txt
@@ -2,9 +2,9 @@
Contributors: colorlib
Requires at least: 6.0
-Tested up to: 7.0
+Tested up to: 7.1
Requires PHP: 7.4
-Stable tag: 2.6.10
+Stable tag: 2.7.1
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Tags: blog, news, e-commerce, one-column, two-columns, left-sidebar, right-sidebar, custom-background, custom-colors, custom-header, editor-style, featured-images, footer-widgets, full-width-template, post-formats, sticky-post, theme-options, block-styles, wide-blocks
@@ -66,15 +66,30 @@ Bootstrap v3.4.1
License: MIT
Source: https://getbootstrap.com/
-FlexSlider v2.7.0
+Bootstrap Glyphicons Halflings (bundled with Bootstrap 3.4.1)
+ Copyright Jan Kovarik
+ License: MIT, as part of Bootstrap
+ Source: https://glyphicons.com/
+
+FlexSlider v2.7.2
Copyright 2012 WooThemes
License: GNU General Public License v2
Source: https://github.com/woocommerce/FlexSlider
-Font Awesome Free v5.1.1
+Font Awesome Free v7.3.1 (complete set, version 4 shim, and the theme's subset)
Copyright Fonticons, Inc.
License: Icons CC BY 4.0, Fonts SIL OFL 1.1, Code MIT
- Source: https://fontawesome.com/license
+ Source: https://fontawesome.com/license/free
+
+Open Sans
+ Copyright 2020 The Open Sans Project Authors
+ License: SIL Open Font License 1.1
+ Source: https://fonts.google.com/specimen/Open+Sans
+
+Roboto Slab
+ Copyright 2018 The Roboto Slab Project Authors
+ License: Apache License 2.0
+ Source: https://fonts.google.com/specimen/Roboto+Slab
Academicons v1.8.6
Copyright James Walsh
@@ -91,6 +106,37 @@ Screenshot image
== Changelog ==
+= 2.7.1 =
+* Fixed keyboard focus reaching invisible slider links. Inactive slides are hidden from assistive technologies and their original tab order is restored when they become active.
+* Added a slideshow pause/play button, stopped automatic rotation on keyboard focus and hover, and honoured reduced motion. Slider controls support the Space key, stay clickable with mixed-height slides, and announce their purpose and current slide.
+* Fixed the call-to-action strip growing excessively tall beneath shorter slides when another slide has a taller image. Slide containment also prevents focus from scrolling the image out of view.
+* Fixed Escape leaving desktop submenus visible while their buttons reported them collapsed. Hover, keyboard focus and buttons share the same menu state. Nested submenu buttons support Space and Escape closes only the nearest submenu.
+* Darkened the default link hover/focus colour to meet text contrast requirements on light backgrounds. Saved custom colours are retained.
+* Content-hashed the subset icon fonts so a cached font from an earlier release cannot hide newly added icons.
+* Added frontend regression tests on jQuery 3.7 and 4.0 and restricted theme archives to runtime files.
+
+= 2.7.0 =
+* Fixed two fatal errors on PHP 8: an Archive Pages headline containing a % other than %s ("Tagged %s at 100%") took the archive page down, and so did a classic [caption] with a non-numeric width.
+* Fixed the mobile menu and the front page slider on jQuery 4 (Bootstrap 3 refused to run, and the slider started through a jQuery API removed in 3.0). The theme works on jQuery 3.7 and on jQuery 4.0 without Migrate.
+* Fixed video posts showing an empty box on the blog, archive and search pages: the embed was stripped by the escaping that ran over it.
+* Fixed 404 pages and empty searches losing their sidebar to the WooCommerce layout when WooCommerce is active.
+* Fixed a fatal "Cannot declare class WP_Bootstrap_Navwalker" with plugins that bundle a nav walker of that name. The theme's walker is Sparkling_Bootstrap_Navwalker; the old name remains an alias.
+* Fixed the password form: it lost WordPress's redirect and "Invalid password" message, pre-filled the field with the search query and was broken up by automatic paragraphs.
+* Fixed a JavaScript error on every in-page link (the obsolete skip-link focus script), and the sticky header sitting under the admin bar, going stale when the menu wrapped, and hiding anchor targets.
+* Fixed the Customizer: the slider category and slide count disagreed with the site, invalid typography values were saved as nothing, the slider's settings fell out of step with its switch, the switches could not be used from the keyboard, the link sections were unstyled, and the theme hid the Customizer's "Change" theme button.
+* Fixed the About page: FancyBox and other plugins with differently named files always showed "Install", install buttons were offered to users who could not install plugins, a hidden tab was a fatal error, and the changelog needed FTP credentials on some hosts.
+* Fixed the Sparkling Categories widget breaking on category names with parentheses and ignoring "Enable Posts Count", empty image links in Popular Posts and post listings, duplicate ids from the social menu and search forms, and the dropdown colours and nested submenu arrow from 768px up.
+* Added theme.json: the block editor gets the theme's palette, which follows your Customizer colours, block buttons use the theme's button colours, and the editor is as wide as the column the post renders in.
+* Added four block patterns (call to action, features, about, testimonial) and a "Blocks (full width, no title)" page template to build pages from them.
+* Added icons for X, Bluesky, Threads, Mastodon, TikTok, WhatsApp, Discord, Reddit, Twitch and Medium to the social menu, and a generic icon for any other link.
+* Accessibility: text meets WCAG AA contrast (the theme red is now #D83B4B, a shade darker; saved colours are kept), links in text are underlined, every page has one h1, landmarks and icon links are named, submenu toggles and the back-to-top control are real buttons.
+* Performance: the slider's files load only on the front page, an empty style block is no longer printed, and only the Font Awesome icons the theme uses load unless your content needs more.
+* Tested up to WordPress 7.1 and PHP 8.5. Development tooling moved from Grunt to npm scripts with PHPCS, ESLint and Stylelint at zero warnings in CI.
+
+= 2.6.11 =
+* Font Awesome icons added to a menu by hand disappeared on update to 2.6.7 or later, which subsetted the bundled Font Awesome to the glyphs the theme draws itself. The theme now detects the icons used in your menus and loads the complete library when one of them needs it, so those menus recover on their own.
+* Added "Font Awesome icons" under Content Options in the Customizer, for icons the menu scan cannot see -- a widget, post content, a page builder. The sparkling_full_fontawesome filter still overrides it.
+
= 2.6.10 =
* Fixed the Customizer failing to open with "Class Epsilon_Section_Recommended_Actions not found". The 2.6.x packages shipped an outdated inc/welcome-screen/welcome-page-setup.php that still used classes removed with the Epsilon framework.
* Restored Appearance > About Sparkling, which the same outdated file never registered.
diff --git a/search.php b/search.php
index 57e16897..25873e87 100644
--- a/search.php
+++ b/search.php
@@ -12,13 +12,15 @@
+ ?>
-
+ ' . get_search_query() . '' );
- ?>
+ ?>
+
' ' . __( 'Newer posts', 'sparkling' ),
- 'next_text' => __( 'Older posts', 'sparkling' ) . ' ',
+ 'prev_text' => ' ' . __( 'Newer posts', 'sparkling' ),
+ 'next_text' => __( 'Older posts', 'sparkling' ) . ' ',
)
);
diff --git a/searchform.php b/searchform.php
index e73925ae..038423a8 100644
--- a/searchform.php
+++ b/searchform.php
@@ -4,14 +4,18 @@
*
* @package sparkling
*/
+
+// A page can hold several search forms (the sidebar widget, the 404 page, the
+// "nothing found" message), so each gets its own id for its label.
+$sparkling_search_field = wp_unique_id( 'search-field-' );
?>
diff --git a/sidebar-footer.php b/sidebar-footer.php
index c3d17f12..875f49d1 100644
--- a/sidebar-footer.php
+++ b/sidebar-footer.php
@@ -15,21 +15,21 @@
// If we made it this far we must have widgets.
?>
-