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
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* 404
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
get_header();
?>
<div class="xshop-container">
<div class="xshop-404">
<p class="xshop-404__code">404</p>
<h1 class="xshop-404__title"><?php esc_html_e('Page not found', 'xshop'); ?></h1>
<p class="xshop-text-muted" style="max-inline-size:48ch;margin-inline:auto"><?php esc_html_e('The page you are looking for does not exist. Try searching or return home.', 'xshop'); ?></p>
<div style="margin-block-start:var(--xshop-space-6);display:flex;gap:var(--xshop-space-3);justify-content:center;flex-wrap:wrap">
<a class="xshop-btn xshop-btn--primary" href="<?php echo esc_url(home_url('/')); ?>"><?php esc_html_e('Go home', 'xshop'); ?></a>
</div>
<div style="max-inline-size:520px;margin-inline:auto;margin-block-start:var(--xshop-space-8)">
<?php get_template_part('template-parts/components/search-form', null, ['placeholder'=>esc_html__('Search…','xshop')]); ?>
</div>
<?php
$recent = new WP_Query(['post_type'=>'post','posts_per_page'=>3,'no_found_rows'=>true]);
if ($recent->have_posts()):
?>
<section class="xshop-section" aria-labelledby="xshop-404-recent">
<h2 id="xshop-404-recent" class="xshop-section__title" style="font-size:var(--xshop-text-xl)"><?php esc_html_e('Recent posts', 'xshop'); ?></h2>
<div class="xshop-grid xshop-grid--posts" style="margin-block-start:var(--xshop-space-4);text-align:start">
<?php while ($recent->have_posts()): $recent->the_post(); get_template_part('template-parts/components/post-card'); endwhile; wp_reset_postdata(); ?>
</div>
</section>
<?php endif; ?>
</div>
</div>
<?php get_footer(); ?>
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* Archive
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
get_header();
?>
<div class="xshop-container">
<div class="xshop-layout">
<div class="xshop-layout__content">
<header class="xshop-page-header">
<?php the_archive_title('<h1 class="xshop-page-header__title">', '</h1>'); ?>
<?php $desc = get_the_archive_description(); if ($desc !== ''): ?>
<div class="xshop-page-header__desc"><?php echo wp_kses_post($desc); ?></div>
<?php endif; ?>
</header>
<?php if (have_posts()): ?>
<div class="xshop-grid xshop-grid--posts">
<?php while (have_posts()): the_post(); get_template_part('template-parts/components/post-card'); endwhile; ?>
</div>
<?php get_template_part('template-parts/components/pagination'); ?>
<?php else: ?>
<?php get_template_part('template-parts/components/empty-state', null, ['title'=>esc_html__('Nothing here','xshop'),'text'=>esc_html__('No posts found in this archive.','xshop'),'cta_label'=>esc_html__('Go home','xshop'),'cta_url'=>home_url('/')]); ?>
<?php endif; ?>
</div>
<?php get_sidebar(); ?>
</div>
</div>
<?php get_footer(); ?>
+36
View File
@@ -0,0 +1,36 @@
/* XShop base — minimal reset + tokens wiring. Logical properties throughout. */
*, *::before, *::after { box-sizing: border-box; }
html { font-family: var(--xshop-font-sans); line-height: var(--xshop-leading); color: var(--xshop-text); background: var(--xshop-bg); -webkit-font-smoothing: antialiased; }
body { margin: 0; background: var(--xshop-bg); color: var(--xshop-text); }
a { color: var(--xshop-link); text-decoration: none; transition: color var(--xshop-transition); }
a:hover { color: var(--xshop-link-hover); }
a:focus-visible { outline: 2px solid var(--xshop-focus); outline-offset: 2px; }
img { max-width: 100%; height: auto; display: block; }
p { margin-block: 0 var(--xshop-space-4); }
h1, h2, h3, h4, h5, h6 { line-height: var(--xshop-leading-tight); margin-block: 0 var(--xshop-space-3); font-weight: var(--xshop-weight-bold); overflow-wrap: break-word; }
ul, ol { margin: 0; padding: 0; }
/* Container */
.xshop-container { max-width: var(--xshop-container); margin-inline: auto; padding-inline: var(--xshop-space-4); }
.xshop-container--narrow { max-width: var(--xshop-container-narrow); }
/* Skip link */
.xshop-skip-link { position: absolute; inset-inline-start: -9999px; inset-block-start: auto; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.xshop-skip-link:focus { inset-inline-start: var(--xshop-space-4); inset-block-start: var(--xshop-space-4); inline-size: auto; block-size: auto; overflow: visible; clip-path: none; white-space: normal; background: var(--xshop-card); color: var(--xshop-text); padding: var(--xshop-space-2) var(--xshop-space-4); border: 2px solid var(--xshop-focus); border-radius: var(--xshop-radius-sm); z-index: var(--xshop-z-modal); font-weight: var(--xshop-weight-medium); }
/* Focus */
:focus-visible { outline: 2px solid var(--xshop-focus); outline-offset: 2px; }
:focus:not(:focus-visible) { outline: none; }
/* Forms */
input, textarea, select, button { font: inherit; }
input[type="search"], input[type="text"], input[type="email"], input[type="password"], input[type="url"], input[type="tel"], textarea, select {
background: var(--xshop-input-bg); color: var(--xshop-input-text); border: 1px solid var(--xshop-input-border);
border-radius: var(--xshop-radius-sm); padding: var(--xshop-space-2) var(--xshop-space-3); inline-size: 100%;
}
input:focus, textarea:focus, select:focus { outline: 2px solid var(--xshop-focus); outline-offset: 0; border-color: var(--xshop-focus); }
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
@@ -0,0 +1,90 @@
/* XShop Components — buttons, cards, breadcrumbs, pagination, notices, etc. */
/* Buttons */
.xshop-btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--xshop-space-2); padding: var(--xshop-space-2) var(--xshop-space-4); border-radius: var(--xshop-radius-sm); border: 1px solid transparent; font-weight: var(--xshop-weight-medium); font-size: var(--xshop-text-sm); cursor: pointer; transition: background var(--xshop-transition), border-color var(--xshop-transition), color var(--xshop-transition); text-decoration: none; line-height: 1.4; }
.xshop-btn--primary { background: var(--xshop-primary); color: #fff; border-color: var(--xshop-primary); }
.xshop-btn--primary:hover { background: var(--xshop-primary-600); border-color: var(--xshop-primary-600); color: #fff; }
.xshop-btn--secondary { background: var(--xshop-card); color: var(--xshop-text); border-color: var(--xshop-border); }
.xshop-btn--secondary:hover { background: var(--xshop-surface); }
.xshop-btn--ghost { background: transparent; color: var(--xshop-text); border-color: transparent; }
.xshop-btn--ghost:hover { background: var(--xshop-surface); }
.xshop-btn:disabled { opacity: .55; cursor: not-allowed; }
/* Card — post-card, product placeholder, generic */
.xshop-card { background: var(--xshop-card); border: 1px solid var(--xshop-border); border-radius: var(--xshop-radius-md); overflow: hidden; display: flex; flex-direction: column; transition: box-shadow var(--xshop-transition); }
.xshop-card:hover { box-shadow: var(--xshop-shadow-md); }
.xshop-card__media { aspect-ratio: 16/10; background: var(--xshop-surface); overflow: hidden; }
.xshop-card__media img { inline-size: 100%; block-size: 100%; object-fit: cover; }
.xshop-card__media--placeholder { display: flex; align-items: center; justify-content: center; color: var(--xshop-muted); font-size: var(--xshop-text-sm); }
.xshop-card__body { padding: var(--xshop-space-4); display: flex; flex-direction: column; gap: var(--xshop-space-2); flex: 1; }
.xshop-card__title { font-size: var(--xshop-text-base); margin: 0; line-height: var(--xshop-leading-tight); }
.xshop-card__title a { color: var(--xshop-text); }
.xshop-card__title a:hover { color: var(--xshop-link); }
.xshop-card__meta { font-size: var(--xshop-text-sm); color: var(--xshop-muted); display: flex; gap: var(--xshop-space-2); flex-wrap: wrap; }
.xshop-card__excerpt { font-size: var(--xshop-text-sm); color: var(--xshop-muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
/* Breadcrumbs */
.xshop-breadcrumbs { font-size: var(--xshop-text-sm); color: var(--xshop-muted); }
.xshop-breadcrumbs ol { display: flex; flex-wrap: wrap; gap: var(--xshop-space-2); list-style: none; margin: 0; padding: 0; align-items: center; }
.xshop-breadcrumbs li { display: inline-flex; align-items: center; gap: var(--xshop-space-2); }
.xshop-breadcrumbs li + li::before { content: "/"; opacity: .5; }
[dir="rtl"] .xshop-breadcrumbs li + li::before { content: "/"; }
.xshop-breadcrumbs a { color: var(--xshop-muted); }
.xshop-breadcrumbs a:hover { color: var(--xshop-text); }
.xshop-breadcrumbs [aria-current="page"] { color: var(--xshop-text); font-weight: var(--xshop-weight-medium); }
/* Pagination */
.xshop-pagination { display: flex; justify-content: center; margin-block-start: var(--xshop-space-8); }
.xshop-pagination .nav-links { display: flex; gap: var(--xshop-space-1); flex-wrap: wrap; align-items: center; }
.xshop-pagination .page-numbers { display: inline-flex; align-items: center; justify-content: center; min-inline-size: 40px; block-size: 40px; padding-inline: var(--xshop-space-2); border: 1px solid var(--xshop-border); border-radius: var(--xshop-radius-sm); background: var(--xshop-card); color: var(--xshop-text); font-size: var(--xshop-text-sm); text-decoration: none; }
.xshop-pagination .page-numbers:hover { background: var(--xshop-surface); }
.xshop-pagination .page-numbers.current { background: var(--xshop-primary); border-color: var(--xshop-primary); color: #fff; font-weight: var(--xshop-weight-bold); }
.xshop-pagination .page-numbers.dots { border-color: transparent; background: transparent; }
/* Search form */
.xshop-search-form { display: flex; gap: var(--xshop-space-2); max-inline-size: 520px; }
.xshop-search-form input[type="search"] { flex: 1; }
/* Empty state */
.xshop-empty { text-align: center; padding: var(--xshop-space-12) var(--xshop-space-6); background: var(--xshop-surface); border: 1px solid var(--xshop-border); border-radius: var(--xshop-radius-md); }
.xshop-empty__title { font-size: var(--xshop-text-xl); margin: 0 0 var(--xshop-space-2); }
.xshop-empty__text { color: var(--xshop-muted); margin: 0 0 var(--xshop-space-4); }
/* Loading */
.xshop-loading { display: inline-flex; align-items: center; gap: var(--xshop-space-2); color: var(--xshop-muted); font-size: var(--xshop-text-sm); }
.xshop-loading__spinner { inline-size: 16px; block-size: 16px; border: 2px solid var(--xshop-border); border-block-start-color: var(--xshop-primary); border-radius: 50%; animation: xshop-spin 600ms linear infinite; }
@keyframes xshop-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .xshop-loading__spinner { animation: none; } }
/* Notices */
.xshop-notice { padding: var(--xshop-space-3) var(--xshop-space-4); border-radius: var(--xshop-radius-sm); border: 1px solid var(--xshop-border); background: var(--xshop-surface); font-size: var(--xshop-text-sm); }
.xshop-notice--success { background: var(--xshop-success-bg); border-color: #86efac; color: #14532d; }
[data-theme="dark"] .xshop-notice--success { color: #bbf7d0; }
.xshop-notice--warning { background: var(--xshop-warning-bg); border-color: #fcd34d; }
.xshop-notice--error { background: var(--xshop-danger-bg); border-color: #fca5a5; }
/* Comments */
.xshop-comments { margin-block-start: var(--xshop-space-8); border-block-start: 1px solid var(--xshop-border); padding-block-start: var(--xshop-space-8); }
.xshop-comments__title { font-size: var(--xshop-text-xl); }
.xshop-comment-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--xshop-space-4); }
.xshop-comment { background: var(--xshop-card); border: 1px solid var(--xshop-border); border-radius: var(--xshop-radius-md); padding: var(--xshop-space-4); }
.xshop-comment__meta { font-size: var(--xshop-text-sm); color: var(--xshop-muted); margin-block-end: var(--xshop-space-2); }
.xshop-comment__body { font-size: var(--xshop-text-base); }
.xshop-comment .children { list-style: none; margin-block-start: var(--xshop-space-4); padding-inline-start: var(--xshop-space-6); display: flex; flex-direction: column; gap: var(--xshop-space-4); border-inline-start: 2px solid var(--xshop-border); }
@media (max-width: 640px) { .xshop-comment .children { padding-inline-start: var(--xshop-space-3); } }
/* Page header */
.xshop-page-header { margin-block-end: var(--xshop-space-6); }
.xshop-page-header__title { font-size: var(--xshop-text-3xl); margin: 0; }
.xshop-page-header__desc { color: var(--xshop-muted); margin-block-start: var(--xshop-space-2); }
/* 404 */
.xshop-404 { text-align: center; padding: var(--xshop-space-16) var(--xshop-space-4); }
.xshop-404__code { font-size: 5rem; font-weight: var(--xshop-weight-bold); line-height: 1; color: var(--xshop-border); margin: 0; }
.xshop-404__title { font-size: var(--xshop-text-3xl); margin: var(--xshop-space-4) 0 var(--xshop-space-2); }
/* Promo banner foundation */
.xshop-promo { background: var(--xshop-surface); border: 1px solid var(--xshop-border); border-radius: var(--xshop-radius-md); padding: var(--xshop-space-6); display: flex; align-items: center; justify-content: space-between; gap: var(--xshop-space-4); flex-wrap: wrap; }
/* Woo notice compat */
.woocommerce-message, .woocommerce-info, .woocommerce-error { border-radius: var(--xshop-radius-sm); }
+82
View File
@@ -0,0 +1,82 @@
/* XShop Layout — header, footer, grid, sections. Logical properties only. */
/* Header */
.xshop-header { position: sticky; inset-block-start: 0; z-index: var(--xshop-z-header); background: var(--xshop-card); border-block-end: 1px solid var(--xshop-border); }
.xshop-header__inner { display: flex; align-items: center; gap: var(--xshop-space-4); padding-block: var(--xshop-space-3); flex-wrap: wrap; }
.xshop-header__branding { display: flex; align-items: center; gap: var(--xshop-space-3); flex-shrink: 0; }
.xshop-header__logo img { max-block-size: 40px; inline-size: auto; }
.xshop-header__title { font-size: var(--xshop-text-xl); font-weight: var(--xshop-weight-bold); margin: 0; }
.xshop-header__title a { color: var(--xshop-text); }
.xshop-header__tagline { font-size: var(--xshop-text-sm); color: var(--xshop-muted); margin: 0; }
.xshop-header__nav { flex: 1 1 auto; display: flex; justify-content: center; }
.xshop-header__actions { display: flex; align-items: center; gap: var(--xshop-space-2); flex-shrink: 0; }
.xshop-header__action-btn { display: inline-flex; align-items: center; justify-content: center; min-inline-size: 40px; min-block-size: 40px; border: 1px solid var(--xshop-border); border-radius: var(--xshop-radius-sm); background: var(--xshop-card); color: var(--xshop-text); cursor: pointer; }
.xshop-header__action-btn:hover { background: var(--xshop-surface); }
.xshop-header__search { position: relative; }
.xshop-header__search-form { display: flex; gap: var(--xshop-space-2); }
/* Primary nav */
.xshop-nav { display: flex; }
.xshop-nav__list { display: flex; flex-wrap: wrap; gap: var(--xshop-space-1); list-style: none; margin: 0; padding: 0; align-items: center; }
.xshop-nav__list a { display: block; padding: var(--xshop-space-2) var(--xshop-space-3); color: var(--xshop-text); border-radius: var(--xshop-radius-sm); font-weight: var(--xshop-weight-medium); font-size: var(--xshop-text-sm); }
.xshop-nav__list a:hover, .xshop-nav__list a:focus-visible { background: var(--xshop-surface); color: var(--xshop-text); }
.xshop-nav__list .current-menu-item > a, .xshop-nav__list .current_page_item > a { background: var(--xshop-surface-2); }
/* Mobile nav */
.xshop-mobile-toggle { display: none; align-items: center; justify-content: center; inline-size: 40px; block-size: 40px; border: 1px solid var(--xshop-border); border-radius: var(--xshop-radius-sm); background: var(--xshop-card); cursor: pointer; color: var(--xshop-text); }
.xshop-mobile-nav { display: none; border-block-start: 1px solid var(--xshop-border); padding-block: var(--xshop-space-3); }
.xshop-mobile-nav.is-open { display: block; }
.xshop-mobile-nav__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.xshop-mobile-nav__list a { display: block; padding: var(--xshop-space-2) var(--xshop-space-3); color: var(--xshop-text); border-radius: var(--xshop-radius-sm); }
.xshop-mobile-nav__list a:hover { background: var(--xshop-surface); }
@media (max-width: 1024px) {
.xshop-nav--desktop { display: none; }
.xshop-mobile-toggle { display: inline-flex; }
.xshop-header__search--desktop { display: none; }
}
/* Footer */
.xshop-footer { background: var(--xshop-surface); border-block-start: 1px solid var(--xshop-border); margin-block-start: var(--xshop-space-12); padding-block: var(--xshop-space-8) var(--xshop-space-6); }
.xshop-footer__widgets { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--xshop-space-6); margin-block-end: var(--xshop-space-8); }
.xshop-footer__bottom { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--xshop-space-4); border-block-start: 1px solid var(--xshop-border); padding-block-start: var(--xshop-space-4); font-size: var(--xshop-text-sm); color: var(--xshop-muted); }
.xshop-footer__nav-list { display: flex; flex-wrap: wrap; gap: var(--xshop-space-3); list-style: none; margin: 0; padding: 0; }
.xshop-footer__nav-list a { color: var(--xshop-muted); }
.xshop-footer__nav-list a:hover { color: var(--xshop-text); }
@media (max-width: 768px) {
.xshop-footer__widgets { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 640px) {
.xshop-footer__widgets { grid-template-columns: 1fr; }
.xshop-footer__bottom { flex-direction: column; align-items: flex-start; }
}
/* Main layout */
.xshop-main { padding-block: var(--xshop-space-8); }
.xshop-layout { display: grid; grid-template-columns: 1fr 320px; gap: var(--xshop-space-8); align-items: start; }
.xshop-layout--no-sidebar { grid-template-columns: 1fr; }
.xshop-layout__content { min-inline-size: 0; }
.xshop-layout__sidebar { position: sticky; inset-block-start: calc(60px + var(--xshop-space-4)); }
@media (max-width: 1024px) {
.xshop-layout { grid-template-columns: 1fr; }
.xshop-layout__sidebar { position: static; }
}
/* Grids */
.xshop-grid { display: grid; gap: var(--xshop-space-4); }
.xshop-grid--posts { grid-template-columns: repeat(3, 1fr); }
.xshop-grid--products { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 1024px) { .xshop-grid--posts, .xshop-grid--products { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px) { .xshop-grid--posts, .xshop-grid--products { grid-template-columns: 1fr; } }
/* Section */
.xshop-section { padding-block: var(--xshop-space-8); }
.xshop-section__header { display: flex; align-items: baseline; justify-content: space-between; gap: var(--xshop-space-4); margin-block-end: var(--xshop-space-6); flex-wrap: wrap; }
.xshop-section__title { font-size: var(--xshop-text-2xl); margin: 0; }
.xshop-section__link { font-size: var(--xshop-text-sm); font-weight: var(--xshop-weight-medium); }
/* Hero foundation */
.xshop-hero { background: var(--xshop-surface); border: 1px solid var(--xshop-border); border-radius: var(--xshop-radius-lg); padding: var(--xshop-space-8); display: grid; gap: var(--xshop-space-4); }
.xshop-hero__title { font-size: var(--xshop-text-4xl); margin: 0; }
.xshop-hero__text { color: var(--xshop-muted); max-inline-size: 60ch; }
.xshop-hero__actions { display: flex; gap: var(--xshop-space-3); flex-wrap: wrap; }
+91
View File
@@ -0,0 +1,91 @@
/* XShop Design Tokens — CSS custom properties. Logical, RTL-safe. */
:root {
/* Palette */
--xshop-primary: #0ea5e9;
--xshop-primary-600: #0284c7;
--xshop-primary-700: #0369a1;
--xshop-secondary: #111827;
--xshop-bg: #ffffff;
--xshop-surface: #f8fafc;
--xshop-surface-2: #f1f5f9;
--xshop-card: #ffffff;
--xshop-text: #0f172a;
--xshop-muted: #64748b;
--xshop-border: #e2e8f0;
--xshop-input-bg: #ffffff;
--xshop-input-border: #cbd5e1;
--xshop-input-text: #0f172a;
--xshop-focus: #0ea5e9;
--xshop-link: #0ea5e9;
--xshop-link-hover: #0284c7;
--xshop-success: #16a34a;
--xshop-success-bg: #f0fdf4;
--xshop-warning: #f59e0b;
--xshop-warning-bg: #fffbeb;
--xshop-danger: #dc2626;
--xshop-danger-bg: #fef2f2;
/* Typography */
--xshop-font-sans: system-ui, -apple-system, "Vazirmatn", "IRANSans", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--xshop-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
--xshop-text-xs: 0.75rem; /* 12px */
--xshop-text-sm: 0.875rem; /* 14px */
--xshop-text-base: 1rem; /* 16px */
--xshop-text-lg: 1.125rem; /* 18px */
--xshop-text-xl: 1.25rem; /* 20px */
--xshop-text-2xl: 1.5rem; /* 24px */
--xshop-text-3xl: 1.875rem;/* 30px */
--xshop-text-4xl: 2.25rem; /* 36px */
--xshop-weight-regular: 400;
--xshop-weight-medium: 500;
--xshop-weight-bold: 700;
--xshop-leading-tight: 1.25;
--xshop-leading: 1.6;
--xshop-leading-relaxed: 1.75;
/* Spacing / Layout */
--xshop-space-1: 4px; --xshop-space-2: 8px; --xshop-space-3: 12px; --xshop-space-4: 16px;
--xshop-space-5: 20px; --xshop-space-6: 24px; --xshop-space-8: 32px; --xshop-space-10: 40px;
--xshop-space-12: 48px; --xshop-space-16: 64px;
--xshop-container: 1280px;
--xshop-container-narrow: 720px;
--xshop-radius-sm: 8px; --xshop-radius-md: 14px; --xshop-radius-lg: 22px; --xshop-radius-full: 9999px;
--xshop-shadow-sm: 0 1px 2px rgba(0,0,0,.06);
--xshop-shadow-md: 0 8px 24px rgba(0,0,0,.08);
--xshop-shadow-lg: 0 16px 40px rgba(0,0,0,.12);
--xshop-z-header: 40; --xshop-z-overlay: 50; --xshop-z-modal: 60;
--xshop-transition-fast: 120ms ease;
--xshop-transition: 180ms ease;
--xshop-transition-slow: 280ms ease;
/* Breakpoints (for reference; media queries use literal values) */
--xshop-bp-sm: 640px;
--xshop-bp-md: 768px;
--xshop-bp-lg: 1024px;
--xshop-bp-xl: 1280px;
--xshop-bp-2xl: 1536px;
}
/* Dark mode — semantic swap, not inversion */
[data-theme="dark"] {
--xshop-bg: #0b1220;
--xshop-surface: #111a2e;
--xshop-surface-2: #1a2542;
--xshop-card: #151f38;
--xshop-text: #e2e8f0;
--xshop-muted: #94a3b8;
--xshop-border: #1e2d4a;
--xshop-input-bg: #0f1a33;
--xshop-input-border: #24365e;
--xshop-input-text: #e2e8f0;
--xshop-focus: #38bdf8;
--xshop-link: #38bdf8;
--xshop-link-hover: #7dd3fc;
--xshop-success-bg: #052e16;
--xshop-warning-bg: #451a03;
--xshop-danger-bg: #450a0a;
}
@media (prefers-reduced-motion: reduce) {
:root { --xshop-transition: 0ms; --xshop-transition-fast: 0ms; --xshop-transition-slow: 0ms; }
}
+10
View File
@@ -0,0 +1,10 @@
/* XShop Utilities — visually hidden, containers, prose. */
.xshop-sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.xshop-prose { max-inline-size: 68ch; }
.xshop-prose :where(p, ul, ol, blockquote) { margin-block-end: var(--xshop-space-4); }
.xshop-prose :where(blockquote) { border-inline-start: 3px solid var(--xshop-border); padding-inline-start: var(--xshop-space-4); color: var(--xshop-muted); }
.xshop-prose :where(a) { text-decoration: underline; text-underline-offset: 3px; }
.xshop-prose--rtl { direction: rtl; }
.xshop-text-muted { color: var(--xshop-muted); }
.xshop-text-center { text-align: center; }
.xshop-stack { display: flex; flex-direction: column; gap: var(--xshop-space-4); }
+82
View File
@@ -0,0 +1,82 @@
/**
* XShop theme bootstrap – vanilla, no jQuery.
* Feature modules are dynamically imported when DOM markers are present.
*/
(function () {
'use strict';
// Dark mode toggle via [data-theme] if enabled.
function initThemeMode() {
var data = window.xshopData;
if (!data) return;
// Theme mode is server-rendered via data-theme on <html>; this handles toggle clicks.
document.addEventListener('click', function (e) {
var btn = e.target.closest('[data-xshop-theme-toggle]');
if (!btn) return;
var html = document.documentElement;
var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', next);
try { localStorage.setItem('xshop_theme', next); } catch (_) {}
// Cookie for server-side rendering on next request.
document.cookie = 'xshop_theme=' + next + '; path=/; max-age=2592000; SameSite=Lax';
});
}
// Minimal focus trap helper for drawers/modals (used by later modules).
window.xshopTrapFocus = function (container) {
var sel = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
var nodes = container.querySelectorAll(sel);
if (!nodes.length) return function () {};
var first = nodes[0], last = nodes[nodes.length - 1];
function onKey(e) {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
container.addEventListener('keydown', onKey);
return function () { container.removeEventListener('keydown', onKey); };
};
function initMobileNav() {
var toggle = document.querySelector('[data-xshop-mobile-toggle]');
var nav = document.getElementById('xshop-mobile-nav');
if (!toggle || !nav) return;
var releaseTrap = null;
function setOpen(open) {
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
nav.classList.toggle('is-open', open);
nav.hidden = !open;
if (open) {
// Move focus into nav.
var firstLink = nav.querySelector('a, button');
if (firstLink) firstLink.focus();
if (window.xshopTrapFocus) releaseTrap = window.xshopTrapFocus(nav);
document.addEventListener('keydown', onDocKey);
} else {
if (releaseTrap) { releaseTrap(); releaseTrap = null; }
document.removeEventListener('keydown', onDocKey);
toggle.focus();
}
}
function onDocKey(e) {
if (e.key === 'Escape') setOpen(false);
}
toggle.addEventListener('click', function () {
var open = toggle.getAttribute('aria-expanded') === 'true';
setOpen(!open);
});
// Close when clicking outside.
document.addEventListener('click', function (e) {
if (nav.hidden) return;
if (nav.contains(e.target) || toggle.contains(e.target)) return;
setOpen(false);
});
}
initThemeMode();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMobileNav);
} else {
initMobileNav();
}
})();
+73
View File
@@ -0,0 +1,73 @@
<?php
/**
* Comments — WP native, threaded, paginated, accessible.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
if (post_password_required()) {
echo '<p class="xshop-notice">' . esc_html__('This post is password protected. Enter the password to view comments.', 'xshop') . '</p>';
return;
}
$hasComments = have_comments();
$commentsOpen = comments_open();
$commentsNumber = get_comments_number();
?>
<section id="comments" class="xshop-comments" aria-label="<?php esc_attr_e('Comments', 'xshop'); ?>">
<?php if ($hasComments): ?>
<h2 class="xshop-comments__title">
<?php
printf(
esc_html(_n('%s Comment', '%s Comments', (int) $commentsNumber, 'xshop')),
esc_html(number_format_i18n((int) $commentsNumber))
);
?>
</h2>
<ol class="xshop-comment-list">
<?php
wp_list_comments([
'style' => 'ol',
'short_ping' => true,
'avatar_size' => 40,
'max_depth' => (int) get_option('thread_comments_depth', 5),
]);
?>
</ol>
<?php
the_comments_pagination([
'prev_text' => is_rtl() ? esc_html__('Next', 'xshop') : esc_html__('Previous', 'xshop'),
'next_text' => is_rtl() ? esc_html__('Previous', 'xshop') : esc_html__('Next', 'xshop'),
'class' => 'xshop-pagination',
]);
?>
<?php if (!$commentsOpen): ?>
<p class="xshop-notice"><?php esc_html_e('Comments are closed.', 'xshop'); ?></p>
<?php endif; ?>
<?php else: ?>
<?php if ($commentsOpen): ?>
<h2 class="xshop-comments__title"><?php esc_html_e('Leave a comment', 'xshop'); ?></h2>
<?php else: ?>
<?php if (!is_page()): ?>
<p class="xshop-notice"><?php esc_html_e('Comments are closed.', 'xshop'); ?></p>
<?php endif; ?>
<?php endif; ?>
<?php endif; ?>
<?php
comment_form([
'title_reply_before' => '<h3 id="reply-title" class="xshop-comments__title">',
'title_reply_after' => '</h3>',
'class_submit' => 'xshop-btn xshop-btn--primary',
'submit_button' => '<button name="%1$s" type="submit" id="%2$s" class="%3$s">%4$s</button>',
]);
?>
</section>
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* Footer — widgets, nav, copyright.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
?>
</main>
<footer class="xshop-footer" role="contentinfo">
<?php get_template_part('template-parts/components/site-footer'); ?>
</footer>
<?php wp_footer(); ?>
</body>
</html>
+105
View File
@@ -0,0 +1,105 @@
<?php
/**
* Front Page — extensible section architecture.
* Static front page: hero + latest posts + promo foundations.
* Blog front page: delegates to index behavior.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
if (is_home()) {
// When front page shows blog posts, behave like index.
get_template_part('index');
return;
}
get_header();
?>
<div class="xshop-container xshop-stack">
<?php
// Hero — filterable so child theme / builder can replace.
$hero = apply_filters('xshop_front_hero_args', [
'title' => get_bloginfo('name') ? get_bloginfo('name') : esc_html__('XShop — Foundation', 'xshop'),
'text' => get_bloginfo('description') ? get_bloginfo('description') : esc_html__('A polished, RTL-ready WooCommerce foundation. Your shop content will appear here.', 'xshop'),
'cta_label' => esc_html__('Browse Shop', 'xshop'),
'cta_url' => function_exists('wc_get_page_permalink') ? wc_get_page_permalink('shop') : home_url('/'),
]);
get_template_part('template-parts/components/hero', null, $hero);
?>
<?php
// Category / content grid foundation — shows top categories when available, else latest content placeholder.
$cats = get_terms(['taxonomy' => 'category', 'hide_empty' => true, 'number' => 6]);
if (!is_wp_error($cats) && !empty($cats)):
?>
<section class="xshop-section" aria-labelledby="xshop-cat-heading">
<?php get_template_part('template-parts/components/section-heading', null, ['title' => esc_html__('Categories', 'xshop'), 'tag' => 'h2']); ?>
<div class="xshop-grid" style="grid-template-columns:repeat(auto-fill,minmax(160px,1fr))">
<?php foreach ($cats as $cat): ?>
<a class="xshop-card" href="<?php echo esc_url(get_term_link($cat)); ?>" style="padding:var(--xshop-space-4);text-align:center">
<strong><?php echo esc_html($cat->name); ?></strong>
<span class="xshop-text-muted" style="font-size:var(--xshop-text-sm)"><?php echo esc_html(sprintf(esc_html__('%s posts', 'xshop'), number_format_i18n((int) $cat->count))); ?></span>
</a>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<?php
// Product/content grid foundation — latest products if WC active, else latest posts.
if (xshop_has_woocommerce() && function_exists('wc_get_products')):
$products = wc_get_products(['limit' => 6, 'status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', 'return' => 'ids']);
if (!empty($products)):
?>
<section class="xshop-section" aria-labelledby="xshop-products-heading">
<?php get_template_part('template-parts/components/section-heading', null, ['title' => esc_html__('Latest Products', 'xshop'), 'link_label' => esc_html__('View shop', 'xshop'), 'link_url' => function_exists('wc_get_page_permalink') ? wc_get_page_permalink('shop') : home_url('/')]); ?>
<div class="xshop-grid xshop-grid--products">
<?php foreach ($products as $pid):
$p = wc_get_product((int) $pid);
if (!$p) { continue; }
?>
<article class="xshop-card">
<a class="xshop-card__media" href="<?php echo esc_url(get_permalink((int) $pid)); ?>" tabindex="-1">
<?php if (has_post_thumbnail((int) $pid)): echo get_the_post_thumbnail((int) $pid, 'xshop-card', ['loading'=>'lazy','decoding'=>'async']); else: ?><span class="xshop-card__media--placeholder xshop-card__media" style="aspect-ratio:16/10;display:flex;align-items:center;justify-content:center"><?php esc_html_e('No image', 'xshop'); ?></span><?php endif; ?>
</a>
<div class="xshop-card__body">
<h3 class="xshop-card__title"><a href="<?php echo esc_url(get_permalink((int) $pid)); ?>"><?php echo esc_html($p->get_name()); ?></a></h3>
<div class="xshop-card__meta"><?php echo wp_kses_post($p->get_price_html()); ?></div>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<?php endif; endif; ?>
<?php
get_template_part('template-parts/components/promo-banner', null, [
'title' => esc_html__('Promotional Banner Foundation', 'xshop'),
'text' => esc_html__('Replace with xshop-core Banners (position: home_hero) in later milestones.', 'xshop'),
'cta_label' => esc_html__('Learn more', 'xshop'),
'cta_url' => home_url('/'),
]);
?>
<?php
// Latest posts foundation — always rendered as content grid fallback.
$latest = new WP_Query(['post_type'=>'post','posts_per_page'=>6,'no_found_rows'=>true,'ignore_sticky_posts'=>true]);
if ($latest->have_posts()):
?>
<section class="xshop-section" aria-labelledby="xshop-latest-heading">
<?php get_template_part('template-parts/components/section-heading', null, ['title' => esc_html__('Latest Posts', 'xshop'), 'link_label' => esc_html__('View blog', 'xshop'), 'link_url' => get_permalink(get_option('page_for_posts')) ?: home_url('/')]); ?>
<div class="xshop-grid xshop-grid--posts">
<?php while ($latest->have_posts()): $latest->the_post(); get_template_part('template-parts/components/post-card'); endwhile; wp_reset_postdata(); ?>
</div>
</section>
<?php else: ?>
<?php get_template_part('template-parts/components/empty-state', null, ['title'=>esc_html__('No posts yet','xshop'),'text'=>esc_html__('Publish posts to see them here.','xshop')]); ?>
<?php endif; ?>
</div>
<?php get_footer(); ?>
+30
View File
@@ -0,0 +1,30 @@
<?php
/**
* XShop Theme Bootstrap
*
* Thin loader – no business logic here. Each module exposes `xshop_*` setup functions.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
define('XSHOP_VERSION', '1.0.0');
define('XSHOP_THEME_DIR', get_template_directory());
define('XSHOP_THEME_URI', get_template_directory_uri());
// Load order matters: helpers first.
require_once XSHOP_THEME_DIR . '/inc/helpers/sanitize.php';
require_once XSHOP_THEME_DIR . '/inc/helpers/template.php';
require_once XSHOP_THEME_DIR . '/inc/setup/theme-support.php';
require_once XSHOP_THEME_DIR . '/inc/setup/menus.php';
require_once XSHOP_THEME_DIR . '/inc/setup/sidebars.php';
require_once XSHOP_THEME_DIR . '/inc/setup/body-classes.php';
require_once XSHOP_THEME_DIR . '/inc/setup/assets.php';
require_once XSHOP_THEME_DIR . '/inc/woocommerce/setup.php';
require_once XSHOP_THEME_DIR . '/inc/compatibility/gutenberg.php';
require_once XSHOP_THEME_DIR . '/inc/compatibility/elementor.php';
require_once XSHOP_THEME_DIR . '/inc/customization/options.php';
require_once XSHOP_THEME_DIR . '/inc/performance/loader.php';
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* Header — semantic, accessible, RTL/LTR safe.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$dark = xshop_is_dark_mode();
?><!doctype html>
<html <?php language_attributes(); ?> data-theme="<?php echo $dark ? 'dark' : 'light'; ?>">
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<a class="xshop-skip-link" href="#xshop-main"><?php esc_html_e('Skip to content', 'xshop'); ?></a>
<header class="xshop-header" role="banner">
<?php get_template_part('template-parts/components/site-header'); ?>
</header>
<main id="xshop-main" class="xshop-main" tabindex="-1">
<?php
// Breadcrumbs on all but front page and 404; Woo pages get Woo-aware breadcrumbs.
if (!is_front_page() && !is_404()) {
echo '<div class="xshop-container" style="margin-block-end:var(--xshop-space-4)">';
get_template_part('template-parts/components/breadcrumbs');
echo '</div>';
}
?>
@@ -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.
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* Index — blog fallback.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
get_header();
?>
<div class="xshop-container">
<div class="xshop-layout">
<div class="xshop-layout__content">
<header class="xshop-page-header">
<h1 class="xshop-page-header__title"><?php esc_html_e('Blog', 'xshop'); ?></h1>
</header>
<?php if (have_posts()): ?>
<div class="xshop-grid xshop-grid--posts">
<?php while (have_posts()): the_post(); ?>
<?php get_template_part('template-parts/components/post-card'); ?>
<?php endwhile; ?>
</div>
<?php get_template_part('template-parts/components/pagination'); ?>
<?php else: ?>
<?php get_template_part('template-parts/components/empty-state', null, ['title' => esc_html__('No posts yet', 'xshop'), 'text' => esc_html__('When you publish posts they will appear here.', 'xshop')]); ?>
<?php endif; ?>
</div>
<?php get_sidebar(); ?>
</div>
</div>
<?php get_footer(); ?>
+50
View File
@@ -0,0 +1,50 @@
<?php
/**
* Page
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
get_header();
?>
<div class="xshop-container">
<div class="xshop-layout">
<div class="xshop-layout__content">
<?php while (have_posts()): the_post(); ?>
<article <?php post_class('xshop-prose'); ?>>
<header class="xshop-page-header">
<h1 class="xshop-page-header__title"><?php the_title(); ?></h1>
</header>
<?php if (has_post_thumbnail()): ?>
<div class="xshop-card__media" style="border-radius:var(--xshop-radius-md);margin-block-end:var(--xshop-space-6)">
<?php the_post_thumbnail('large', ['loading'=>'eager','decoding'=>'async']); ?>
</div>
<?php endif; ?>
<div class="entry-content">
<?php
the_content();
wp_link_pages([
'before' => '<nav class="xshop-pagination" aria-label="' . esc_attr__('Page links', 'xshop') . '"><span class="xshop-text-muted">' . esc_html__('Pages:', 'xshop') . '</span> ',
'after' => '</nav>',
]);
?>
</div>
</article>
<?php
if (comments_open() || (int) get_comments_number() > 0) {
comments_template();
}
?>
<?php endwhile; ?>
</div>
<?php get_sidebar(); ?>
</div>
</div>
<?php get_footer(); ?>
+2
View File
@@ -0,0 +1,2 @@
/* RTL fallback — tokens use logical properties; this file exists for WordPress RTL detection. */
body.rtl { direction: rtl; }
+1
View File
@@ -0,0 +1 @@
PNG placeholder – replace with 1200x900 screenshot before release. See tools/build-release.ps1 notes.
+43
View File
@@ -0,0 +1,43 @@
<?php
/**
* Search Results
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
get_header();
$query = get_search_query();
?>
<div class="xshop-container">
<div class="xshop-layout">
<div class="xshop-layout__content">
<header class="xshop-page-header">
<h1 class="xshop-page-header__title">
<?php if ($query !== ''): printf(esc_html__('Search results for: %s', 'xshop'), '<span>' . esc_html($query) . '</span>'); else: esc_html_e('Search', 'xshop'); endif; ?>
</h1>
<div style="margin-block-start:var(--xshop-space-4)">
<?php get_template_part('template-parts/components/search-form', null, ['placeholder'=>esc_html__('Search…','xshop')]); ?>
</div>
<?php if (have_posts()): ?>
<p class="xshop-text-muted" style="margin-block-start:var(--xshop-space-3)"><?php echo esc_html(sprintf(esc_html__('%s results found', 'xshop'), number_format_i18n((int) $GLOBALS['wp_query']->found_posts))); ?></p>
<?php endif; ?>
</header>
<?php if (have_posts()): ?>
<div class="xshop-grid xshop-grid--posts">
<?php while (have_posts()): the_post(); get_template_part('template-parts/components/post-card'); endwhile; ?>
</div>
<?php get_template_part('template-parts/components/pagination'); ?>
<?php else: ?>
<?php get_template_part('template-parts/components/empty-state', null, ['title'=>esc_html__('No results','xshop'),'text'=>esc_html__('Try different keywords or browse categories.','xshop'),'cta_label'=>esc_html__('Go home','xshop'),'cta_url'=>home_url('/')]); ?>
<?php endif; ?>
</div>
<?php get_sidebar(); ?>
</div>
</div>
<?php get_footer(); ?>
+23
View File
@@ -0,0 +1,23 @@
<?php
/**
* Sidebar — dynamic_sidebar with graceful empty state.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$sidebarId = $args['id'] ?? 'sidebar-main';
if (!is_active_sidebar($sidebarId)) {
// Allow filtering to suppress empty state.
if (apply_filters('xshop_sidebar_show_empty', false, $sidebarId)) {
echo '<aside class="xshop-layout__sidebar" aria-label="' . esc_attr__('Sidebar', 'xshop') . '"><p class="xshop-text-muted">' . esc_html__('No widgets added.', 'xshop') . '</p></aside>';
}
return;
}
?>
<aside class="xshop-layout__sidebar" aria-label="<?php esc_attr_e('Sidebar', 'xshop'); ?>">
<?php dynamic_sidebar($sidebarId); ?>
</aside>
+78
View File
@@ -0,0 +1,78 @@
<?php
/**
* Single Post
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
get_header();
?>
<div class="xshop-container">
<div class="xshop-layout">
<div class="xshop-layout__content">
<?php while (have_posts()): the_post(); ?>
<article <?php post_class('xshop-prose'); ?>>
<header class="xshop-page-header">
<h1 class="xshop-page-header__title"><?php the_title(); ?></h1>
<div class="xshop-card__meta">
<time datetime="<?php echo esc_attr(get_the_date('c')); ?>"><?php echo esc_html(get_the_date()); ?></time>
<span aria-hidden="true">·</span>
<span><?php echo esc_html(get_the_author()); ?></span>
<?php
$cats = get_the_category();
if (!empty($cats[0])) {
echo '<span aria-hidden="true">·</span><a href="' . esc_url(get_category_link($cats[0]->term_id)) . '">' . esc_html($cats[0]->name) . '</a>';
}
?>
</div>
</header>
<?php if (has_post_thumbnail()): ?>
<div class="xshop-card__media" style="border-radius:var(--xshop-radius-md);margin-block-end:var(--xshop-space-6)">
<?php the_post_thumbnail('large', ['loading'=>'eager','decoding'=>'async']); ?>
</div>
<?php endif; ?>
<div class="entry-content">
<?php
the_content();
wp_link_pages([
'before' => '<nav class="xshop-pagination" aria-label="' . esc_attr__('Page links', 'xshop') . '"><span class="xshop-text-muted">' . esc_html__('Pages:', 'xshop') . '</span> ',
'after' => '</nav>',
]);
?>
</div>
<?php
$tags = get_the_tags();
if ($tags):
?>
<footer style="margin-block-start:var(--xshop-space-6)">
<p class="xshop-card__meta"><?php the_tags(esc_html__('Tags: ', 'xshop'), ', '); ?></p>
</footer>
<?php endif; ?>
</article>
<?php
// Next/previous post navigation (accessible).
the_post_navigation([
'prev_text' => '<span class="xshop-text-muted">' . esc_html__('Previous', 'xshop') . '</span><br>%title',
'next_text' => '<span class="xshop-text-muted">' . esc_html__('Next', 'xshop') . '</span><br>%title',
]);
?>
<?php
if (comments_open() || (int) get_comments_number() > 0) {
comments_template();
}
?>
<?php endwhile; ?>
</div>
<?php get_sidebar(); ?>
</div>
</div>
<?php get_footer(); ?>
+16
View File
@@ -0,0 +1,16 @@
/*
Theme Name: XShop
Theme URI: https://example.com/xshop
Author: XShop Team
Author URI: https://example.com
Description: Persian-first, RTL/LTR, responsive WooCommerce theme. Fast, accessible, and commercial-grade. Requires xshop-core companion plugin for wishlist/compare/Q&A/search/filter/banners.
Version: 1.0.0
License: Commercial
License URI: https://example.com/license
Text Domain: xshop
Domain Path: /languages
Requires at least: 6.4
Tested up to: 6.6
Requires PHP: 8.2
Tags: woocommerce, rtl, e-commerce, persian, responsive, dark-mode, accessibility-ready
*/
@@ -0,0 +1,113 @@
<?php
/**
* Breadcrumbs — semantic, RTL/LTR safe, Woo-aware, SEO-friendly.
*
* Data: array of ['label'=>string,'url'=>string|null,'current'=>bool]
* If $args['items'] not provided, auto-builds from WP/WC context.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$args = wp_parse_args($args ?? [], ['items' => null]);
$items = $args['items'];
if ($items === null) {
$items = [];
// Home.
$items[] = ['label' => esc_html__('Home', 'xshop'), 'url' => home_url('/'), 'current' => false];
if (is_front_page()) {
// Front page is current; keep only home as current.
$items[0]['current'] = true;
$items[0]['url'] = null;
} elseif (function_exists('is_woocommerce') && xshop_has_woocommerce() && (is_woocommerce() || is_cart() || is_checkout() || is_account_page())) {
// Delegate to Woo breadcrumb if available, but provide fallback.
if (function_exists('is_shop') && is_shop()) {
$items[] = ['label' => esc_html__('Shop', 'xshop'), 'url' => null, 'current' => true];
} elseif (function_exists('is_product_category') && is_product_category()) {
$shopUrl = function_exists('wc_get_page_permalink') ? wc_get_page_permalink('shop') : home_url('/');
$items[] = ['label' => esc_html__('Shop', 'xshop'), 'url' => $shopUrl, 'current' => false];
$term = get_queried_object();
if ($term instanceof WP_Term) {
$items[] = ['label' => $term->name, 'url' => null, 'current' => true];
}
} elseif (function_exists('is_product') && is_product()) {
$shopUrl = function_exists('wc_get_page_permalink') ? wc_get_page_permalink('shop') : home_url('/');
$items[] = ['label' => esc_html__('Shop', 'xshop'), 'url' => $shopUrl, 'current' => false];
$items[] = ['label' => get_the_title(), 'url' => null, 'current' => true];
} elseif (function_exists('is_cart') && is_cart()) {
$items[] = ['label' => esc_html__('Cart', 'xshop'), 'url' => null, 'current' => true];
} elseif (function_exists('is_checkout') && is_checkout()) {
$items[] = ['label' => esc_html__('Checkout', 'xshop'), 'url' => null, 'current' => true];
} elseif (function_exists('is_account_page') && is_account_page()) {
$items[] = ['label' => esc_html__('My Account', 'xshop'), 'url' => null, 'current' => true];
}
} elseif (is_home()) {
$postsPage = get_option('page_for_posts');
$label = $postsPage ? get_the_title((int) $postsPage) : esc_html__('Blog', 'xshop');
$items[] = ['label' => $label, 'url' => null, 'current' => true];
} elseif (is_category() || is_tag() || is_tax()) {
$term = get_queried_object();
if ($term instanceof WP_Term) {
$items[] = ['label' => $term->name, 'url' => null, 'current' => true];
}
} elseif (is_search()) {
$items[] = ['label' => sprintf(esc_html__('Search: %s', 'xshop'), get_search_query()), 'url' => null, 'current' => true];
} elseif (is_404()) {
$items[] = ['label' => esc_html__('Not Found', 'xshop'), 'url' => null, 'current' => true];
} elseif (is_singular()) {
$postType = get_post_type();
if ($postType && $postType !== 'post' && $postType !== 'page') {
$obj = get_post_type_object($postType);
if ($obj) {
$items[] = ['label' => $obj->labels->name, 'url' => null, 'current' => false];
}
} elseif ($postType === 'post') {
$cat = get_the_category();
if (!empty($cat[0])) {
$items[] = ['label' => $cat[0]->name, 'url' => get_category_link($cat[0]->term_id), 'current' => false];
}
}
$items[] = ['label' => get_the_title(), 'url' => null, 'current' => true];
} elseif (is_archive()) {
$items[] = ['label' => get_the_archive_title(), 'url' => null, 'current' => true];
}
// Mark last as current if none marked.
$hasCurrent = false;
foreach ($items as $it) { if (!empty($it['current'])) { $hasCurrent = true; break; } }
if (!$hasCurrent && count($items) > 0) {
$items[count($items) - 1]['current'] = true;
$items[count($items) - 1]['url'] = null;
}
}
// Never break if empty.
if (empty($items)) { return; }
// Allow SEO plugins / child themes to filter.
$items = apply_filters('xshop_breadcrumbs_items', $items);
if (empty($items) || !is_array($items)) { return; }
?>
<nav class="xshop-breadcrumbs" aria-label="<?php esc_attr_e('Breadcrumb', 'xshop'); ?>">
<ol>
<?php foreach ($items as $item):
$label = isset($item['label']) ? (string) $item['label'] : '';
if ($label === '') { continue; }
$isCurrent = !empty($item['current']);
$url = isset($item['url']) ? (string) $item['url'] : '';
?>
<li>
<?php if ($isCurrent || $url === ''): ?>
<span aria-current="page"><?php echo esc_html($label); ?></span>
<?php else: ?>
<a href="<?php echo esc_url($url); ?>"><?php echo esc_html($label); ?></a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</nav>
@@ -0,0 +1,23 @@
<?php
/**
* Empty state — no posts / no results.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$title = $args['title'] ?? esc_html__('Nothing found', 'xshop');
$text = $args['text'] ?? esc_html__('Try adjusting your search or browse categories.', 'xshop');
$ctaLabel = $args['cta_label'] ?? '';
$ctaUrl = $args['cta_url'] ?? '';
?>
<div class="xshop-empty" role="status">
<h2 class="xshop-empty__title"><?php echo esc_html($title); ?></h2>
<p class="xshop-empty__text"><?php echo esc_html($text); ?></p>
<?php if ($ctaLabel !== '' && $ctaUrl !== ''): ?>
<a class="xshop-btn xshop-btn--primary" href="<?php echo esc_url($ctaUrl); ?>"><?php echo esc_html($ctaLabel); ?></a>
<?php endif; ?>
</div>
@@ -0,0 +1,27 @@
<?php
/**
* Hero foundation — extensible, no hardcoded business copy beyond fallbacks.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$title = $args['title'] ?? esc_html__('Welcome to XShop', 'xshop');
$text = $args['text'] ?? esc_html__('A fast, RTL-ready store foundation. Customize hero content via front-page.php or a child theme.', 'xshop');
$ctaLabel = $args['cta_label'] ?? '';
$ctaUrl = $args['cta_url'] ?? '';
?>
<section class="xshop-hero" aria-label="<?php esc_attr_e('Hero', 'xshop'); ?>">
<h1 class="xshop-hero__title"><?php echo esc_html($title); ?></h1>
<?php if ($text !== ''): ?>
<p class="xshop-hero__text"><?php echo esc_html($text); ?></p>
<?php endif; ?>
<?php if ($ctaLabel !== '' && $ctaUrl !== ''): ?>
<div class="xshop-hero__actions">
<a class="xshop-btn xshop-btn--primary" href="<?php echo esc_url($ctaUrl); ?>"><?php echo esc_html($ctaLabel); ?></a>
</div>
<?php endif; ?>
</section>
@@ -0,0 +1,17 @@
<?php
/**
* Loading indicator — for future AJAX use; static markup safe for M1.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$label = $args['label'] ?? esc_html__('Loading…', 'xshop');
?>
<div class="xshop-loading" role="status" aria-live="polite" aria-busy="true">
<span class="xshop-loading__spinner" aria-hidden="true"></span>
<span><?php echo esc_html($label); ?></span>
</div>
@@ -0,0 +1,41 @@
<?php
/**
* Pagination — archive/search, accessible, RTL-safe.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
global $wp_query;
$query = $args['query'] ?? $wp_query;
if (!$query instanceof WP_Query) { return; }
if ((int) $query->max_num_pages <= 1) { return; }
$big = 999999999;
$links = paginate_links([
'base' => str_replace((string) $big, '%#%', esc_url(get_pagenum_link((string) $big))),
'format' => '?paged=%#%',
'current' => max(1, (int) get_query_var('paged', 1)),
'total' => (int) $query->max_num_pages,
'prev_text' => is_rtl() ? esc_html__('Next', 'xshop') : esc_html__('Previous', 'xshop'),
'next_text' => is_rtl() ? esc_html__('Previous', 'xshop') : esc_html__('Next', 'xshop'),
'type' => 'array',
'mid_size' => 1,
'end_size' => 1,
]);
if (empty($links) || !is_array($links)) { return; }
?>
<nav class="xshop-pagination" aria-label="<?php esc_attr_e('Pagination', 'xshop'); ?>">
<div class="nav-links">
<?php foreach ($links as $link):
// paginate_links returns escaped HTML; we allow it via wp_kses with minimal allow-list
// but current span vs anchor is already safe. Use wp_kses_post to preserve classes.
echo wp_kses_post($link);
endforeach; ?>
</div>
</nav>
@@ -0,0 +1,45 @@
<?php
/**
* Post Card — reusable, handles missing thumb/excerpt/long title.
*
* Expects $args['post_id'] or uses global $post.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$postId = isset($args['post_id']) ? (int) $args['post_id'] : get_the_ID();
if (!$postId) { return; }
$title = get_the_title($postId);
if ($title === '') { $title = esc_html__('Untitled', 'xshop'); }
$permalink = get_permalink($postId);
$excerpt = has_excerpt($postId) ? get_the_excerpt($postId) : wp_trim_words(wp_strip_all_tags(get_post_field('post_content', $postId)), 22);
$hasThumb = has_post_thumbnail($postId);
?>
<article <?php post_class('xshop-card'); ?>>
<a href="<?php echo esc_url($permalink); ?>" class="xshop-card__media <?php echo $hasThumb ? '' : 'xshop-card__media--placeholder'; ?>" aria-hidden="<?php echo $hasThumb ? 'false' : 'true'; ?>" tabindex="-1">
<?php if ($hasThumb): ?>
<?php echo get_the_post_thumbnail($postId, 'xshop-card', ['loading' => 'lazy', 'decoding' => 'async', 'alt' => esc_attr($title)]); ?>
<?php else: ?>
<span><?php esc_html_e('No image', 'xshop'); ?></span>
<?php endif; ?>
</a>
<div class="xshop-card__body">
<h3 class="xshop-card__title"><a href="<?php echo esc_url($permalink); ?>"><?php echo esc_html($title); ?></a></h3>
<div class="xshop-card__meta">
<time datetime="<?php echo esc_attr(get_the_date('c', $postId)); ?>"><?php echo esc_html(get_the_date('', $postId)); ?></time>
<?php
$cats = get_the_category($postId);
if (!empty($cats[0])) {
echo '<span aria-hidden="true">·</span><a href="' . esc_url(get_category_link($cats[0]->term_id)) . '">' . esc_html($cats[0]->name) . '</a>';
}
?>
</div>
<?php if ($excerpt !== ''): ?>
<p class="xshop-card__excerpt"><?php echo esc_html($excerpt); ?></p>
<?php endif; ?>
</div>
</article>
@@ -0,0 +1,26 @@
<?php
/**
* Promotional banner foundation — rendered via banner CPT later; static variant for M1.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$title = $args['title'] ?? '';
$text = $args['text'] ?? '';
$ctaLabel = $args['cta_label'] ?? '';
$ctaUrl = $args['cta_url'] ?? '';
if ($title === '' && $text === '') { return; }
?>
<section class="xshop-promo" aria-label="<?php esc_attr_e('Promotion', 'xshop'); ?>">
<div>
<?php if ($title !== ''): ?><h2 class="xshop-section__title" style="font-size:var(--xshop-text-xl);margin:0 0 var(--xshop-space-1)"><?php echo esc_html($title); ?></h2><?php endif; ?>
<?php if ($text !== ''): ?><p style="margin:0;color:var(--xshop-muted)"><?php echo esc_html($text); ?></p><?php endif; ?>
</div>
<?php if ($ctaLabel !== '' && $ctaUrl !== ''): ?>
<a class="xshop-btn xshop-btn--primary" href="<?php echo esc_url($ctaUrl); ?>"><?php echo esc_html($ctaLabel); ?></a>
<?php endif; ?>
</section>
@@ -0,0 +1,20 @@
<?php
/**
* Search form — accessible, RTL-safe.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$uid = 'xshop-search-' . wp_unique_id();
$placeholder = $args['placeholder'] ?? esc_html__('Search…', 'xshop');
$action = $args['action'] ?? home_url('/');
?>
<form role="search" method="get" action="<?php echo esc_url($action); ?>" class="xshop-search-form">
<label class="xshop-sr-only" for="<?php echo esc_attr($uid); ?>"><?php esc_html_e('Search', 'xshop'); ?></label>
<input type="search" id="<?php echo esc_attr($uid); ?>" name="s" value="<?php echo esc_attr(get_search_query()); ?>" placeholder="<?php echo esc_attr($placeholder); ?>" required />
<button type="submit" class="xshop-btn xshop-btn--primary"><?php esc_html_e('Search', 'xshop'); ?></button>
</form>
@@ -0,0 +1,25 @@
<?php
/**
* Section Heading — reusable for home sections.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$title = $args['title'] ?? '';
if ($title === '') { return; }
$linkLabel = $args['link_label'] ?? '';
$linkUrl = $args['link_url'] ?? '';
$tag = $args['tag'] ?? 'h2';
$allowed = ['h2','h3','div'];
if (!in_array($tag, $allowed, true)) { $tag = 'h2'; }
?>
<div class="xshop-section__header">
<<?php echo esc_attr($tag); ?> class="xshop-section__title"><?php echo esc_html($title); ?></<?php echo esc_attr($tag); ?>>
<?php if ($linkLabel !== '' && $linkUrl !== ''): ?>
<a class="xshop-section__link" href="<?php echo esc_url($linkUrl); ?>"><?php echo esc_html($linkLabel); ?></a>
<?php endif; ?>
</div>
@@ -0,0 +1,53 @@
<?php
/**
* Site Footer — widgets, nav, branding fallback, copyright.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$branding = xshop_get_branding_fallback();
$year = date_i18n('Y');
?>
<div class="xshop-container">
<div class="xshop-footer__widgets">
<?php for ($i = 1; $i <= 4; $i++):
$id = 'footer-' . $i;
$has = is_active_sidebar($id);
?>
<div class="xshop-footer__col">
<?php if ($has): ?>
<?php dynamic_sidebar($id); ?>
<?php else: ?>
<section class="xshop-widget">
<h3 class="xshop-widget__title"><?php echo esc_html(sprintf(esc_html__('Footer %d', 'xshop'), $i)); ?></h3>
<p class="xshop-text-muted" style="font-size:var(--xshop-text-sm)"><?php esc_html_e('Add widgets in Appearance → Widgets.', 'xshop'); ?></p>
</section>
<?php endif; ?>
</div>
<?php endfor; ?>
</div>
<div class="xshop-footer__bottom">
<p style="margin:0">
<?php
// Translators: %1$s year, %2$s site name.
printf(esc_html__('© %1$s %2$s. All rights reserved.', 'xshop'), esc_html($year), esc_html($branding['name']));
?>
</p>
<nav aria-label="<?php esc_attr_e('Footer', 'xshop'); ?>">
<?php
wp_nav_menu([
'theme_location' => 'xshop-footer',
'container' => false,
'menu_class' => 'xshop-footer__nav-list',
'depth' => 1,
'fallback_cb' => false,
]);
?>
</nav>
</div>
</div>
@@ -0,0 +1,80 @@
<?php
/**
* Site Header — branding, primary nav, mobile nav, search, WC hooks.
* Used by header.php. Future Header Builder will extend, not replace.
*
* @package XShop
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$branding = xshop_get_branding_fallback();
$hasLogo = has_custom_logo();
?>
<div class="xshop-header__inner xshop-container">
<div class="xshop-header__branding">
<?php if ($hasLogo): ?>
<div class="xshop-header__logo"><?php the_custom_logo(); ?></div>
<?php endif; ?>
<div>
<p class="xshop-header__title"><a href="<?php echo esc_url(home_url('/')); ?>" rel="home"><?php echo esc_html($branding['name']); ?></a></p>
<?php if ($branding['desc'] !== ''): ?>
<p class="xshop-header__tagline"><?php echo esc_html($branding['desc']); ?></p>
<?php endif; ?>
</div>
</div>
<nav class="xshop-header__nav xshop-nav xshop-nav--desktop" aria-label="<?php esc_attr_e('Primary', 'xshop'); ?>">
<?php
wp_nav_menu([
'theme_location' => 'xshop-primary',
'container' => false,
'menu_class' => 'xshop-nav__list',
'fallback_cb' => static function (): void {
echo '<ul class="xshop-nav__list"><li><a href="' . esc_url(home_url('/')) . '">' . esc_html__('Home', 'xshop') . '</a></li></ul>';
},
'depth' => 2,
]);
?>
</nav>
<div class="xshop-header__actions">
<div class="xshop-header__search xshop-header__search--desktop">
<?php get_template_part('template-parts/components/search-form', null, ['placeholder' => esc_html__('Search…', 'xshop')]); ?>
</div>
<?php if (xshop_has_woocommerce()): ?>
<a class="xshop-header__action-btn" href="<?php echo esc_url(function_exists('wc_get_page_permalink') ? wc_get_page_permalink('myaccount') : wp_login_url()); ?>" aria-label="<?php esc_attr_e('My Account', 'xshop'); ?>">
<span aria-hidden="true">◉</span><span class="xshop-sr-only"><?php esc_html_e('My Account', 'xshop'); ?></span>
</a>
<?php if (function_exists('wc_get_cart_url')): ?>
<a class="xshop-header__action-btn" href="<?php echo esc_url(wc_get_cart_url()); ?>" aria-label="<?php esc_attr_e('Cart', 'xshop'); ?>">
<span aria-hidden="true">🛒</span><span class="xshop-sr-only"><?php esc_html_e('Cart', 'xshop'); ?></span>
</a>
<?php endif; ?>
<?php endif; ?>
<button type="button" class="xshop-mobile-toggle" data-xshop-mobile-toggle aria-expanded="false" aria-controls="xshop-mobile-nav" aria-label="<?php esc_attr_e('Toggle menu', 'xshop'); ?>">
<span aria-hidden="true">☰</span>
</button>
</div>
</div>
<nav id="xshop-mobile-nav" class="xshop-mobile-nav xshop-container" aria-label="<?php esc_attr_e('Mobile', 'xshop'); ?>" hidden>
<?php
wp_nav_menu([
'theme_location' => 'xshop-primary',
'container' => false,
'menu_class' => 'xshop-mobile-nav__list',
'fallback_cb' => static function (): void {
echo '<ul class="xshop-mobile-nav__list"><li><a href="' . esc_url(home_url('/')) . '">' . esc_html__('Home', 'xshop') . '</a></li></ul>';
},
'depth' => 2,
]);
?>
<div style="margin-block-start:var(--xshop-space-4)">
<?php get_template_part('template-parts/components/search-form', null, ['placeholder' => esc_html__('Search…', 'xshop')]); ?>
</div>
</nav>