chore: complete M1.1 runtime QA and release packaging
This commit is contained in:
@@ -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(); ?>
|
||||
@@ -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(); ?>
|
||||
@@ -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); }
|
||||
@@ -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; }
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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); }
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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(); ?>
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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(); ?>
|
||||
@@ -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(); ?>
|
||||
@@ -0,0 +1,2 @@
|
||||
/* RTL fallback — tokens use logical properties; this file exists for WordPress RTL detection. */
|
||||
body.rtl { direction: rtl; }
|
||||
@@ -0,0 +1 @@
|
||||
PNG placeholder – replace with 1200x900 screenshot before release. See tools/build-release.ps1 notes.
|
||||
@@ -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(); ?>
|
||||
@@ -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>
|
||||
@@ -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(); ?>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user