chore: complete M1.1 runtime QA and release packaging
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Elementor compatibility – no hard dependency.
|
||||
*/
|
||||
|
||||
add_action('init', 'xshop_elementor_compat');
|
||||
|
||||
function xshop_elementor_compat(): void {
|
||||
// Declare Elementor locations if needed later (header/footer).
|
||||
// Guarded so missing Elementor is harmless.
|
||||
if (!defined('ELEMENTOR_VERSION')) { return; }
|
||||
// No-op for v1; widgets live in xshop-core when Elementor is present.
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
add_action('init', 'xshop_gutenberg_setup');
|
||||
|
||||
function xshop_gutenberg_setup(): void {
|
||||
// Allow wide/full alignment where theme supports it later via theme.json.
|
||||
add_theme_support('align-wide');
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Theme options store: single option `xshop_settings`.
|
||||
* Full options UI ships in M6; this file only registers the setting with sanitization.
|
||||
*/
|
||||
|
||||
add_action('init', 'xshop_register_settings');
|
||||
|
||||
function xshop_register_settings(): void {
|
||||
register_setting('xshop_settings', 'xshop_settings', [
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'xshop_sanitize_settings',
|
||||
'default' => xshop_default_settings(),
|
||||
'show_in_rest' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
function xshop_default_settings(): array {
|
||||
return [
|
||||
'color_mode' => 'light', // light|dark|auto
|
||||
'header_layout' => 'classic', // classic|centered|minimal
|
||||
'footer_layout' => 'four-col',
|
||||
'shop_columns' => 3,
|
||||
'product_card' => 'default',
|
||||
'enable_dark_mode' => '0',
|
||||
'custom_css' => '',
|
||||
];
|
||||
}
|
||||
|
||||
function xshop_sanitize_settings($value): array {
|
||||
if (!is_array($value)) { $value = []; }
|
||||
$defaults = xshop_default_settings();
|
||||
$out = $defaults;
|
||||
if (isset($value['color_mode'])) {
|
||||
$out['color_mode'] = in_array($value['color_mode'], ['light','dark','auto'], true) ? $value['color_mode'] : $defaults['color_mode'];
|
||||
}
|
||||
if (isset($value['header_layout'])) {
|
||||
$out['header_layout'] = in_array($value['header_layout'], ['classic','centered','minimal'], true) ? $value['header_layout'] : $defaults['header_layout'];
|
||||
}
|
||||
if (isset($value['footer_layout'])) {
|
||||
$out['footer_layout'] = sanitize_key((string) $value['footer_layout']);
|
||||
}
|
||||
if (isset($value['shop_columns'])) {
|
||||
$out['shop_columns'] = min(6, max(1, absint($value['shop_columns'])));
|
||||
}
|
||||
if (isset($value['product_card'])) {
|
||||
$out['product_card'] = in_array($value['product_card'], ['compact','default','detailed'], true) ? $value['product_card'] : $defaults['product_card'];
|
||||
}
|
||||
if (isset($value['enable_dark_mode'])) {
|
||||
$out['enable_dark_mode'] = $value['enable_dark_mode'] ? '1' : '0';
|
||||
}
|
||||
if (isset($value['custom_css'])) {
|
||||
// Allow CSS – wp_strip_all_tags + wp_kses with empty allow-list would strip everything;
|
||||
// use sanitization that preserves CSS: wp_strip_all_tags is too aggressive, so allow raw but trim.
|
||||
$out['custom_css'] = trim(wp_strip_all_tags((string) $value['custom_css']));
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Shared sanitization helpers. All options/banners/QA go through these.
|
||||
*/
|
||||
|
||||
function xshop_sanitize_url(?string $value): string {
|
||||
$value = trim((string) $value);
|
||||
return $value === '' ? '' : esc_url_raw($value);
|
||||
}
|
||||
|
||||
function xshop_sanitize_banner_position(string $value): string {
|
||||
$allowed = ['home_hero','home_strip','shop_top','product_sidebar','footer_top'];
|
||||
return in_array($value, $allowed, true) ? $value : 'home_hero';
|
||||
}
|
||||
|
||||
function xshop_sanitize_checkbox($value): string {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Template helpers.
|
||||
*/
|
||||
|
||||
function xshop_get_setting(string $key, $default = null) {
|
||||
static $cache = null;
|
||||
if ($cache === null) {
|
||||
$cache = get_option('xshop_settings', []);
|
||||
if (!is_array($cache)) { $cache = []; }
|
||||
}
|
||||
return $cache[$key] ?? $default;
|
||||
}
|
||||
|
||||
function xshop_svg(string $name, array $attrs = []): string {
|
||||
$attrsStr = '';
|
||||
foreach ($attrs as $k => $v) {
|
||||
$attrsStr .= ' ' . esc_attr((string) $k) . '="' . esc_attr((string) $v) . '"';
|
||||
}
|
||||
// Sprite reference: /assets/images/sprite.svg#icon-<name>
|
||||
$href = esc_url(XSHOP_THEME_URI . '/assets/images/sprite.svg#icon-' . sanitize_key($name));
|
||||
return '<svg' . $attrsStr . ' aria-hidden="true"><use href="' . $href . '"></use></svg>';
|
||||
}
|
||||
|
||||
function xshop_is_dark_mode(): bool {
|
||||
$mode = xshop_get_setting('color_mode', 'light');
|
||||
if ($mode === 'dark') { return true; }
|
||||
if ($mode === 'auto' && isset($_COOKIE['xshop_theme'])) {
|
||||
return $_COOKIE['xshop_theme'] === 'dark';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function xshop_has_woocommerce(): bool {
|
||||
return class_exists('WooCommerce');
|
||||
}
|
||||
|
||||
function xshop_is_woocommerce_page(): bool {
|
||||
if (!xshop_has_woocommerce()) { return false; }
|
||||
return function_exists('is_woocommerce') && (is_woocommerce() || is_cart() || is_checkout() || is_account_page());
|
||||
}
|
||||
|
||||
function xshop_get_branding_fallback(): array {
|
||||
$name = get_bloginfo('name');
|
||||
$desc = get_bloginfo('description');
|
||||
return [
|
||||
'name' => $name ? $name : esc_html__('XShop', 'xshop'),
|
||||
'desc' => $desc ? $desc : '',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Performance helpers – conditional asset loading, defer hints.
|
||||
*/
|
||||
|
||||
add_filter('script_loader_tag', 'xshop_defer_scripts', 10, 3);
|
||||
|
||||
function xshop_defer_scripts(string $tag, string $handle, string $src): string {
|
||||
// Defer theme script (not admin, not customizer preview).
|
||||
if ($handle === 'xshop' && !is_admin() && !is_customize_preview()) {
|
||||
if (str_contains($tag, ' defer')) { return $tag; }
|
||||
$tag = str_replace('<script ', '<script defer ', $tag);
|
||||
}
|
||||
return $tag;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
add_action('wp_enqueue_scripts', 'xshop_enqueue_assets');
|
||||
|
||||
function xshop_enqueue_assets(): void {
|
||||
$ver = defined('XSHOP_VERSION') ? XSHOP_VERSION : '1.0.0';
|
||||
|
||||
// Tokens → base → layout → components → utilities → style.css
|
||||
wp_enqueue_style('xshop-tokens', XSHOP_THEME_URI . '/assets/css/tokens.css', [], $ver);
|
||||
wp_enqueue_style('xshop-base', XSHOP_THEME_URI . '/assets/css/base.css', ['xshop-tokens'], $ver);
|
||||
wp_enqueue_style('xshop-layout', XSHOP_THEME_URI . '/assets/css/layout.css', ['xshop-base'], $ver);
|
||||
wp_enqueue_style('xshop-components', XSHOP_THEME_URI . '/assets/css/components.css', ['xshop-layout'], $ver);
|
||||
wp_enqueue_style('xshop-utilities', XSHOP_THEME_URI . '/assets/css/utilities.css', ['xshop-components'], $ver);
|
||||
wp_enqueue_style('xshop-style', get_stylesheet_uri(), ['xshop-utilities'], $ver);
|
||||
wp_style_add_data('xshop-style', 'rtl', 'replace');
|
||||
|
||||
// Conditionally avoid WooCommerce styles on non-WC pages – WC styles already dequeued in inc/woocommerce/setup.php.
|
||||
// Theme JS is tiny; feature modules conditionally enqueued elsewhere.
|
||||
wp_enqueue_script('xshop', XSHOP_THEME_URI . '/assets/js/theme.js', [], $ver, true);
|
||||
|
||||
// Localized data for REST/AJAX (nonces, i18n hints).
|
||||
wp_localize_script('xshop', 'xshopData', [
|
||||
'restUrl' => esc_url_raw(rest_url('xshop/v1/')),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'isRtl' => is_rtl(),
|
||||
'i18n' => [
|
||||
'searchPlaceholder' => esc_html__('Search products…', 'xshop'),
|
||||
'noResults' => esc_html__('No results found.', 'xshop'),
|
||||
],
|
||||
]);
|
||||
|
||||
if (is_singular() && comments_open() && get_option('thread_comments')) {
|
||||
wp_enqueue_script('comment-reply');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
add_filter('body_class', 'xshop_body_classes');
|
||||
|
||||
function xshop_body_classes(array $classes): array {
|
||||
$classes[] = 'xshop';
|
||||
$classes[] = xshop_is_dark_mode() ? 'xshop--dark' : 'xshop--light';
|
||||
if (xshop_has_woocommerce()) {
|
||||
$classes[] = 'xshop--has-woo';
|
||||
if (xshop_is_woocommerce_page()) {
|
||||
$classes[] = 'xshop--woo-page';
|
||||
}
|
||||
} else {
|
||||
$classes[] = 'xshop--no-woo';
|
||||
}
|
||||
if (is_rtl()) {
|
||||
$classes[] = 'xshop--rtl';
|
||||
}
|
||||
return $classes;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
add_action('after_setup_theme', 'xshop_register_menus');
|
||||
|
||||
function xshop_register_menus(): void {
|
||||
register_nav_menus([
|
||||
'xshop-primary' => esc_html__('Primary Menu', 'xshop'),
|
||||
'xshop-mobile' => esc_html__('Mobile Menu', 'xshop'),
|
||||
'xshop-footer' => esc_html__('Footer Menu', 'xshop'),
|
||||
'xshop-topbar' => esc_html__('Top Bar Menu', 'xshop'),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
add_action('widgets_init', 'xshop_register_sidebars');
|
||||
|
||||
function xshop_register_sidebars(): void {
|
||||
$sidebars = [
|
||||
['id'=>'sidebar-main','name'=>__('Main Sidebar','xshop'),'desc'=>__('Appears on blog and pages.','xshop')],
|
||||
['id'=>'sidebar-shop','name'=>__('Shop Sidebar','xshop'),'desc'=>__('Appears on shop archives.','xshop')],
|
||||
['id'=>'footer-1','name'=>__('Footer Column 1','xshop'),'desc'=>''],
|
||||
['id'=>'footer-2','name'=>__('Footer Column 2','xshop'),'desc'=>''],
|
||||
['id'=>'footer-3','name'=>__('Footer Column 3','xshop'),'desc'=>''],
|
||||
['id'=>'footer-4','name'=>__('Footer Column 4','xshop'),'desc'=>''],
|
||||
];
|
||||
foreach ($sidebars as $s) {
|
||||
register_sidebar([
|
||||
'name' => $s['name'],
|
||||
'id' => $s['id'],
|
||||
'description' => $s['desc'],
|
||||
'before_widget' => '<section id="%1$s" class="xshop-widget %2$s">',
|
||||
'after_widget' => '</section>',
|
||||
'before_title' => '<h3 class="xshop-widget__title">',
|
||||
'after_title' => '</h3>',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
add_action('after_setup_theme', 'xshop_setup_theme_support');
|
||||
|
||||
function xshop_setup_theme_support(): void {
|
||||
add_theme_support('title-tag');
|
||||
add_theme_support('post-thumbnails');
|
||||
add_theme_support('html5', ['search-form','comment-form','comment-list','gallery','caption','style','script']);
|
||||
add_theme_support('custom-logo', ['height'=>56,'width'=>200,'flex-height'=>true,'flex-width'=>true]);
|
||||
add_theme_support('automatic-feed-links');
|
||||
add_theme_support('responsive-embeds');
|
||||
add_theme_support('editor-styles');
|
||||
add_theme_support('wp-block-styles');
|
||||
add_theme_support('woocommerce', [
|
||||
'thumbnail_image_width' => 400,
|
||||
'single_image_width' => 800,
|
||||
]);
|
||||
add_theme_support('wc-product-gallery-zoom');
|
||||
add_theme_support('wc-product-gallery-lightbox');
|
||||
add_theme_support('wc-product-gallery-slider');
|
||||
|
||||
load_theme_textdomain('xshop', XSHOP_THEME_DIR . '/languages');
|
||||
|
||||
// Image sizes.
|
||||
add_image_size('xshop-card', 400, 400, false);
|
||||
add_image_size('xshop-card-2x', 800, 800, false);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* WooCommerce integration: wrappers, hooks. Keep template overrides minimal.
|
||||
*/
|
||||
|
||||
add_action('after_setup_theme', 'xshop_woo_setup');
|
||||
|
||||
function xshop_woo_setup(): void {
|
||||
if (!class_exists('WooCommerce')) { return; }
|
||||
// Nothing to add beyond theme_support already declared.
|
||||
// Hook wrappers if WooCommerce is active.
|
||||
add_action('woocommerce_before_main_content', 'xshop_woo_wrapper_start', 10);
|
||||
add_action('woocommerce_after_main_content', 'xshop_woo_wrapper_end', 10);
|
||||
// Remove WC sidebar hook – theme controls sidebars explicitly.
|
||||
remove_action('woocommerce_sidebar', 'woocommerce_get_sidebar', 10);
|
||||
}
|
||||
|
||||
function xshop_woo_wrapper_start(): void {
|
||||
echo '<main id="primary" class="xshop-main xshop-container"><div class="xshop-woo-layout">';
|
||||
}
|
||||
|
||||
function xshop_woo_wrapper_end(): void {
|
||||
echo '</div></main>';
|
||||
}
|
||||
|
||||
add_filter('woocommerce_enqueue_styles', '__return_empty_array');
|
||||
|
||||
// Notices: ensure they render inside theme container when not using WC wrappers.
|
||||
add_action('wp', 'xshop_woo_notice_compat');
|
||||
|
||||
function xshop_woo_notice_compat(): void {
|
||||
if (!class_exists('WooCommerce')) { return; }
|
||||
// No-op for M1; placeholder for future notice positioning hooks.
|
||||
}
|
||||
Reference in New Issue
Block a user