chore: complete M1.1 runtime QA and release packaging

This commit is contained in:
XShop
2026-09-13 19:04:41 +03:30
commit f11e1d650d
66 changed files with 3094 additions and 0 deletions
@@ -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;
}
+21
View File
@@ -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';
}
+53
View File
@@ -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;
}
+37
View File
@@ -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;
}
+14
View File
@@ -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'),
]);
}
+27
View File
@@ -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.
}