feat: Enhance gallery functionality with lightbox feature and improve icon sprite accessibility

- Added a lightbox feature for gallery images, allowing users to view photos in a modal with navigation controls.
- Updated the SVG sprite to include inline styles for better accessibility.
- Increased CSS specificity by doubling the `.vc-app` class to ensure styles override Astra theme defaults.
- Introduced new shortcodes for bus tours, about section, and articles in the VC_Sections class.
- Improved the handling of featured images in the single tour template for sharper display.
- Updated version number to 1.8.15.
This commit is contained in:
2026-09-23 17:26:56 +03:30
parent b0e302be15
commit d5686ed14b
19 changed files with 1700 additions and 684 deletions

Binary file not shown.

View File

@@ -421,6 +421,7 @@
} }
.hoho .ring-badge--top .ring-badge__text { .hoho .ring-badge--top .ring-badge__text {
fill: #808080;
font-size: 0.625rem; font-size: 0.625rem;
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
@@ -546,15 +547,26 @@
box-shadow: 0 1.25rem 2.5rem rgba(0, 0, 0, 0.18); box-shadow: 0 1.25rem 2.5rem rgba(0, 0, 0, 0.18);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateY(-0.375rem); transform: translateY(-0.4rem) scale(0.97);
transition: opacity 0.2s var(--ease), transform 0.2s var(--ease), visibility 0.2s; transform-origin: top center;
/* Exit: fade/slide out, then flip visibility only once the fade is done, so
the close animates instead of snapping. */
transition:
opacity 0.16s var(--ease),
transform 0.16s var(--ease),
visibility 0s linear 0.16s;
} }
.hoho .nav__item--has-menu:hover .nav__submenu, .hoho .nav__item--has-menu:hover .nav__submenu,
.hoho .nav__toggle[aria-expanded="true"]+.nav__submenu { .hoho .nav__toggle[aria-expanded="true"]+.nav__submenu {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateY(0); transform: translateY(0) scale(1);
/* Enter: a slightly longer, soft ease-out so it settles in gently. */
transition:
opacity 0.24s var(--ease),
transform 0.28s cubic-bezier(0.16, 1, 0.3, 1),
visibility 0s;
} }
.hoho .nav__submenu a { .hoho .nav__submenu a {
@@ -1868,14 +1880,24 @@
box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.22); box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.22);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateY(-0.5rem); transform: translateY(-0.6rem) scale(0.98);
transition: opacity 0.25s var(--ease), transform 0.25s var(--ease), visibility 0.25s; transform-origin: top right;
/* Exit: fade/slide up, then hide once the fade completes. */
transition:
opacity 0.18s var(--ease),
transform 0.18s var(--ease),
visibility 0s linear 0.18s;
} }
.hoho .nav.is-open { .hoho .nav.is-open {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateY(0); transform: translateY(0) scale(1);
/* Enter: soft ease-out settle. */
transition:
opacity 0.26s var(--ease),
transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
visibility 0s;
} }
.hoho .nav__list { .hoho .nav__list {

View File

@@ -1,4 +1,4 @@
<svg class="sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg"> <svg class="sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0;overflow:hidden">
<defs> <defs>
<path id="ringPathScroll" fill="none" d="M50,50 m-39,0 a39,39 0 1,1 78,0 a39,39 0 1,1 -78,0" /> <path id="ringPathScroll" fill="none" d="M50,50 m-39,0 a39,39 0 1,1 78,0 a39,39 0 1,1 -78,0" />
</defs> </defs>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -23,10 +23,10 @@ add_action( 'wp_enqueue_scripts', function () {
if ( wp_style_is( 'astra-theme-css', 'registered' ) ) { if ( wp_style_is( 'astra-theme-css', 'registered' ) ) {
$deps[] = 'astra-theme-css'; $deps[] = 'astra-theme-css';
} }
wp_enqueue_style( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.css', $deps, '1.0.2' ); wp_enqueue_style( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.css', $deps, '1.0.8' );
// Header interactions: burger, submenu, sticky, back-to-top, search toggle. // Header interactions: burger, submenu, sticky, back-to-top, search toggle.
wp_enqueue_script( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.js', array(), '1.0.2', true ); wp_enqueue_script( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.js', array(), '1.0.8', true );
wp_add_inline_style( wp_add_inline_style(
'hoho-chrome', 'hoho-chrome',
@@ -40,9 +40,17 @@ add_action( 'wp_enqueue_scripts', function () {
'.hoho,.hoho h1,.hoho h2,.hoho h3,.hoho p,.hoho a,.hoho span,.hoho li,.hoho button,.hoho input{font-family:"Plus Jakarta Sans",system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}', '.hoho,.hoho h1,.hoho h2,.hoho h3,.hoho p,.hoho a,.hoho span,.hoho li,.hoho button,.hoho input{font-family:"Plus Jakarta Sans",system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}',
// Normalize: stop Astra decorating header/footer links & lists. // Normalize: stop Astra decorating header/footer links & lists.
'.hoho a,.hoho.hoho a{text-decoration:none!important;box-shadow:none!important;}', '.hoho a,.hoho.hoho a{text-decoration:none!important;box-shadow:none!important;}',
'.hoho a:not(.btn){color:inherit;}', // …but not ring-badges: they set their own colour on the <a>, and this
// inherit rule out-specifies .ring-badge--top, turning the footer
// "back to top" text white (inherited) on the light area above it.
'.hoho a:not(.btn):not(.ring-badge){color:inherit;}',
'.hoho :is(ul,ol){list-style:none!important;margin:0!important;padding:0!important;}', '.hoho :is(ul,ol){list-style:none!important;margin:0!important;padding:0!important;}',
'.hoho li{margin:0!important;}', '.hoho li{margin:0!important;}',
// Let the footer\'s decorative bits (back-to-top ring, support tab)
// spill vertically, but keep the horizontal axis clipped so nothing
// causes a mobile sideways scroll. overflow-x:clip (unlike hidden)
// does not force overflow-y to auto, so the y-axis stays visible.
'.hoho,.hoho .footer{overflow-x:clip;overflow-y:visible;}',
// Icon buttons: Astra gives <button> a filled background + its own // Icon buttons: Astra gives <button> a filled background + its own
// colour, which turned the header search icon into an empty dark // colour, which turned the header search icon into an empty dark
// circle. Reset them (real .btn buttons keep their styling). // circle. Reset them (real .btn buttons keep their styling).
@@ -51,9 +59,11 @@ add_action( 'wp_enqueue_scripts', function () {
'.hoho button:not(.btn):not(.burger){background:transparent;border:0;color:inherit;}', '.hoho button:not(.btn):not(.burger){background:transparent;border:0;color:inherit;}',
// Expanding header search field. // Expanding header search field.
'.hoho .header__search{display:flex;align-items:center;gap:.25rem;}', '.hoho .header__search{display:flex;align-items:center;gap:.25rem;}',
'.hoho .header__search-input{width:0;min-width:0;padding:0;border:0;outline:0;opacity:0;height:var(--control-h);border-radius:var(--r-pill);background:rgba(255,255,255,.15);color:#fff;transition:width .25s var(--ease),opacity .2s var(--ease),padding .25s var(--ease);}', // !important beats Astra's input[type=search] styling, which forces a
// white field background (and, with a white placeholder, hid the text).
'.hoho .header__search-input{width:0;min-width:0;padding:0;border:0!important;outline:0;opacity:0;height:var(--control-h);border-radius:var(--r-pill)!important;background:rgba(255,255,255,.15)!important;color:#fff!important;box-shadow:none!important;transition:width .25s var(--ease),opacity .2s var(--ease),padding .25s var(--ease);}',
'.hoho .header__search.is-open .header__search-input{width:min(52vw,15rem);padding:0 1rem;opacity:1;}', '.hoho .header__search.is-open .header__search-input{width:min(52vw,15rem);padding:0 1rem;opacity:1;}',
'.hoho .header__search-input::placeholder{color:rgba(255,255,255,.7);}', '.hoho .header__search-input::placeholder{color:rgba(255,255,255,.7)!important;opacity:1;}',
) )
) )
); );
@@ -105,9 +115,11 @@ add_action( 'wp', function () {
} }
add_action( 'astra_header', 'hoho_render_header' ); add_action( 'astra_header', 'hoho_render_header' );
if ( function_exists( 'astra_footer_markup' ) ) { // Astra renders its footer via the Footer Builder (a class method on the
remove_action( 'astra_footer', 'astra_footer_markup' ); // `astra_footer` hook), so removing the legacy astra_footer_markup() alone
} // left a second, default footer. Clear every callback on the hook, then add
// ours — a clean, full replacement regardless of classic vs builder footer.
remove_all_actions( 'astra_footer' );
add_action( 'astra_footer', 'hoho_render_footer' ); add_action( 'astra_footer', 'hoho_render_footer' );
} ); } );
@@ -146,7 +158,10 @@ function hoho_logo() {
* Small helper: inline-icon markup referencing the sprite. * Small helper: inline-icon markup referencing the sprite.
*/ */
function hoho_icon( $name, $extra = '' ) { function hoho_icon( $name, $extra = '' ) {
$name = preg_replace( '/[^a-z0-9\-]/', '', 'i-' . ltrim( (string) $name, 'i-' ) ); // Strip an existing "i-" prefix (once), then re-add it. ltrim() with a
// charlist would eat any leading i/-, turning "instagram" into "nstagram".
$slug = preg_replace( '/^i-/', '', (string) $name );
$name = preg_replace( '/[^a-z0-9\-]/', '', 'i-' . $slug );
$class = 'icon' . ( $extra ? ' ' . $extra : '' ); $class = 'icon' . ( $extra ? ' ' . $extra : '' );
return '<svg class="' . esc_attr( $class ) . '" aria-hidden="true"><use href="#' . esc_attr( $name ) . '"></use></svg>'; return '<svg class="' . esc_attr( $class ) . '" aria-hidden="true"><use href="#' . esc_attr( $name ) . '"></use></svg>';
} }

View File

@@ -39,7 +39,10 @@ $hoho_footer_col = function ( $location ) {
<footer class="footer" id="footer"> <footer class="footer" id="footer">
<a class="ring-badge ring-badge--top" href="#top" aria-label="<?php echo esc_attr__( 'Back to top', 'hoponhopoff' ); ?>"> <a class="ring-badge ring-badge--top" href="#top" aria-label="<?php echo esc_attr__( 'Back to top', 'hoponhopoff' ); ?>">
<svg class="ring-badge__ring" viewBox="0 0 100 100" aria-hidden="true"> <svg class="ring-badge__ring" viewBox="0 0 100 100" aria-hidden="true">
<text class="ring-badge__text"><textPath href="#ringPathScroll" startOffset="0">BACK TO TOP · BACK TO TOP · </textPath></text> <text class="ring-badge__text" text-anchor="middle">
<textPath href="#ringPathScroll" startOffset="25%"><?php echo esc_html__( 'BACK TO TOP', 'hoponhopoff' ); ?></textPath>
<textPath href="#ringPathScroll" startOffset="75%"><?php echo esc_html__( 'BACK TO TOP', 'hoponhopoff' ); ?></textPath>
</text>
</svg> </svg>
<span class="ring-badge__dot"><svg class="icon" aria-hidden="true"><use href="#i-arrow-up"></use></svg></span> <span class="ring-badge__dot"><svg class="icon" aria-hidden="true"><use href="#i-arrow-up"></use></svg></span>
</a> </a>

View File

@@ -4,7 +4,7 @@ Theme URI: https://git.kaamyab.dev/vargha/TourWebsite
Description: Astra child theme that replaces the Astra header and footer with the "Hop On Hop Off" design. Pairs with the Viator Collector plugin. Description: Astra child theme that replaces the Astra header and footer with the "Hop On Hop Off" design. Pairs with the Viator Collector plugin.
Author: Amir Bagheri Author: Amir Bagheri
Template: astra Template: astra
Version: 1.0.2 Version: 1.0.8
Text Domain: hoponhopoff Text Domain: hoponhopoff
*/ */

View File

@@ -58,7 +58,15 @@ html {
body { body {
margin: 0; margin: 0;
font-family: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family:
"Plus Jakarta Sans",
system-ui,
-apple-system,
"Segoe UI",
Roboto,
Helvetica,
Arial,
sans-serif;
font-size: var(--fs-body); font-size: var(--fs-body);
line-height: 1.5; line-height: 1.5;
color: var(--c-ink); color: var(--c-ink);
@@ -173,7 +181,10 @@ dd {
border-radius: var(--r-pill); border-radius: var(--r-pill);
font-weight: 700; font-weight: 700;
white-space: nowrap; white-space: nowrap;
transition: transform 0.2s var(--ease), filter 0.2s var(--ease), background 0.2s var(--ease); transition:
transform 0.2s var(--ease),
filter 0.2s var(--ease),
background 0.2s var(--ease);
} }
.btn:hover { .btn:hover {
@@ -378,7 +389,9 @@ dd {
.rail:hover .dcard.is-active:not(:hover):not(:focus-within) .ring-badge__dot, .rail:hover .dcard.is-active:not(:hover):not(:focus-within) .ring-badge__dot,
.rail:hover .acard.is-active:not(:hover):not(:focus-within) .ring-badge__dot, .rail:hover .acard.is-active:not(:hover):not(:focus-within) .ring-badge__dot,
.bus__grid:hover .tcard.is-active:not(:hover):not(:focus-within) .ring-badge__dot { .bus__grid:hover
.tcard.is-active:not(:hover):not(:focus-within)
.ring-badge__dot {
color: var(--c-white); color: var(--c-white);
background: var(--c-glass); background: var(--c-glass);
} }
@@ -430,7 +443,6 @@ dd {
} }
@keyframes nudge { @keyframes nudge {
0%, 0%,
60%, 60%,
100% { 100% {
@@ -541,11 +553,14 @@ dd {
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateY(-0.375rem); transform: translateY(-0.375rem);
transition: opacity 0.2s var(--ease), transform 0.2s var(--ease), visibility 0.2s; transition:
opacity 0.2s var(--ease),
transform 0.2s var(--ease),
visibility 0.2s;
} }
.nav__item--has-menu:hover .nav__submenu, .nav__item--has-menu:hover .nav__submenu,
.nav__toggle[aria-expanded="true"]+.nav__submenu { .nav__toggle[aria-expanded="true"] + .nav__submenu {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateY(0); transform: translateY(0);
@@ -585,7 +600,9 @@ dd {
height: 0.125rem; height: 0.125rem;
border-radius: 1px; border-radius: 1px;
background: var(--c-white); background: var(--c-white);
transition: transform 0.25s var(--ease), opacity 0.25s var(--ease); transition:
transform 0.25s var(--ease),
opacity 0.25s var(--ease);
} }
.burger[aria-expanded="true"] span:nth-child(1) { .burger[aria-expanded="true"] span:nth-child(1) {
@@ -607,9 +624,9 @@ dd {
position: relative; position: relative;
display: grid; display: grid;
align-items: end; align-items: end;
min-height: clamp(34rem, 88svh, 56rem);
margin: var(--page-inset); margin: var(--page-inset);
padding-block: calc(var(--header-h) + var(--sp-6)) clamp(3rem, 1.5rem + 6vw, 6rem); padding-block: calc(var(--header-h) + var(--sp-6))
clamp(3rem, 1.5rem + 6vw, 6rem);
border-radius: var(--r-xl); border-radius: var(--r-xl);
isolation: isolate; isolation: isolate;
} }
@@ -634,7 +651,11 @@ dd {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: background:
radial-gradient(39.91% 77.37% at 60.22% 50%, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%), radial-gradient(
39.91% 77.37% at 60.22% 50%,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.5) 100%
),
linear-gradient(180deg, rgba(44, 175, 194, 0) 51.6%, var(--c-teal) 100%); linear-gradient(180deg, rgba(44, 175, 194, 0) 51.6%, var(--c-teal) 100%);
} }
@@ -814,24 +835,28 @@ dd {
scroll-snap-type: x mandatory; scroll-snap-type: x mandatory;
scroll-behavior: smooth; scroll-behavior: smooth;
scrollbar-width: none; scrollbar-width: none;
/* negative margin + padding keeps the card glow from being clipped by the scroller */ /* overflow-x:auto forces overflow-y to auto, so the hover glow (a ~3.75rem
margin-inline: calc(var(--gap) * -1); blur) would be clipped by the scroller. Don't switch to overflow:visible —
padding: var(--sp-4) var(--gap); that kills the horizontal scroll. Instead give the rail enough padding for
the glow to paint INSIDE the scroll box, and pull it back with an equal
negative margin so the visible spacing is unchanged. Scrolling stays intact. */
margin: calc(var(--sp-5) * -1) calc(var(--gap) * -1);
padding: 4.5rem var(--gap);
} }
.rail::-webkit-scrollbar { .rail::-webkit-scrollbar {
display: none; display: none;
} }
.rail>* { .rail > * {
scroll-snap-align: start; scroll-snap-align: start;
} }
.rail--dest>* { .rail--dest > * {
flex: 0 0 calc((100% - 4 * var(--gap)) / 5); flex: 0 0 calc((100% - 4 * var(--gap)) / 5);
} }
.rail--art>* { .rail--art > * {
flex: 0 0 calc((100% - 2 * var(--gap)) / 3); flex: 0 0 calc((100% - 2 * var(--gap)) / 3);
} }
@@ -870,7 +895,11 @@ dd {
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: -1; z-index: -1;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0.65) 100%); background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 35%,
rgba(0, 0, 0, 0.65) 100%
);
} }
/* highlighted card — gradients copied from Figma */ /* highlighted card — gradients copied from Figma */
@@ -884,7 +913,11 @@ dd {
.dcard:hover::after, .dcard:hover::after,
.dcard:focus-within::after { .dcard:focus-within::after {
background: background:
linear-gradient(180deg, rgba(255, 255, 255, 0) 51.6%, rgba(255, 255, 255, 0.4) 100%), linear-gradient(
180deg,
rgba(255, 255, 255, 0) 51.6%,
rgba(255, 255, 255, 0.4) 100%
),
linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)),
linear-gradient(180deg, rgba(44, 175, 194, 0) 37.33%, #2cafc2 84.53%); linear-gradient(180deg, rgba(44, 175, 194, 0) 37.33%, #2cafc2 84.53%);
} }
@@ -897,7 +930,11 @@ dd {
.rail:hover .dcard.is-active:not(:hover):not(:focus-within)::after, .rail:hover .dcard.is-active:not(:hover):not(:focus-within)::after,
.rail:focus-within .dcard.is-active:not(:hover):not(:focus-within)::after { .rail:focus-within .dcard.is-active:not(:hover):not(:focus-within)::after {
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0.65) 100%); background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 35%,
rgba(0, 0, 0, 0.65) 100%
);
} }
.dcard__desc { .dcard__desc {
@@ -907,7 +944,9 @@ dd {
top: clamp(2.75rem, 2rem + 2vw, 3.5rem); top: clamp(2.75rem, 2rem + 2vw, 3.5rem);
opacity: 0; opacity: 0;
transform: translateY(-0.375rem); transform: translateY(-0.375rem);
transition: opacity 0.25s var(--ease), transform 0.25s var(--ease); transition:
opacity 0.25s var(--ease),
transform 0.25s var(--ease);
} }
.dcard.is-active .dcard__desc, .dcard.is-active .dcard__desc,
@@ -918,7 +957,9 @@ dd {
} }
.rail:hover .dcard.is-active:not(:hover):not(:focus-within) .dcard__desc, .rail:hover .dcard.is-active:not(:hover):not(:focus-within) .dcard__desc,
.rail:focus-within .dcard.is-active:not(:hover):not(:focus-within) .dcard__desc { .rail:focus-within
.dcard.is-active:not(:hover):not(:focus-within)
.dcard__desc {
opacity: 0; opacity: 0;
transform: translateY(-0.375rem); transform: translateY(-0.375rem);
} }
@@ -1039,7 +1080,8 @@ dd {
object-fit: contain; object-fit: contain;
object-position: left; object-position: left;
/* Figma tints the blue coach towards the teal brand colour */ /* Figma tints the blue coach towards the teal brand colour */
filter: hue-rotate(-32deg) saturate(1.15) drop-shadow(0 1rem 1.5rem rgba(0, 0, 0, 0.18)); filter: hue-rotate(-32deg) saturate(1.15)
drop-shadow(0 1rem 1.5rem rgba(0, 0, 0, 0.18));
} }
.promo__note { .promo__note {
@@ -1085,7 +1127,12 @@ dd {
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: -1; z-index: -1;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.1) 45%, rgba(0, 0, 0, 0.6) 100%); background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.45) 0%,
rgba(0, 0, 0, 0.1) 45%,
rgba(0, 0, 0, 0.6) 100%
);
} }
.tcard.is-active, .tcard.is-active,
@@ -1098,7 +1145,11 @@ dd {
.tcard:hover::after, .tcard:hover::after,
.tcard:focus-within::after { .tcard:focus-within::after {
background: background:
radial-gradient(53.59% 137.34% at 50% 46.41%, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.4) 100%), radial-gradient(
53.59% 137.34% at 50% 46.41%,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.4) 100%
),
linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)),
linear-gradient(180deg, rgba(44, 175, 194, 0) 37.33%, #2cafc2 84.53%); linear-gradient(180deg, rgba(44, 175, 194, 0) 37.33%, #2cafc2 84.53%);
} }
@@ -1110,7 +1161,12 @@ dd {
.bus__grid:hover .tcard.is-active:not(:hover):not(:focus-within)::after, .bus__grid:hover .tcard.is-active:not(:hover):not(:focus-within)::after,
.bus__grid:focus-within .tcard.is-active:not(:hover):not(:focus-within)::after { .bus__grid:focus-within .tcard.is-active:not(:hover):not(:focus-within)::after {
background: linear-gradient(180deg, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.1) 45%, rgba(0, 0, 0, 0.6) 100%); background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.45) 0%,
rgba(0, 0, 0, 0.1) 45%,
rgba(0, 0, 0, 0.6) 100%
);
} }
.tcard__k { .tcard__k {
@@ -1164,11 +1220,13 @@ dd {
right: -34%; right: -34%;
top: 42%; top: 42%;
height: 28%; height: 28%;
background: linear-gradient(90deg, background: linear-gradient(
rgba(44, 175, 194, 0) 0%, 90deg,
rgba(44, 175, 194, 0.16) 22%, rgba(44, 175, 194, 0) 0%,
rgba(44, 175, 194, 0.2) 55%, rgba(44, 175, 194, 0.16) 22%,
rgba(44, 175, 194, 0) 100%); rgba(44, 175, 194, 0.2) 55%,
rgba(44, 175, 194, 0) 100%
);
/* small blur only — the band keeps its straight parallel edges */ /* small blur only — the band keeps its straight parallel edges */
filter: blur(2px); filter: blur(2px);
/* 25deg == the roof ridge angle, measured off the artwork itself */ /* 25deg == the roof ridge angle, measured off the artwork itself */
@@ -1295,8 +1353,20 @@ dd {
.marquee { .marquee {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); -webkit-mask-image: linear-gradient(
mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); 90deg,
transparent,
#000 8%,
#000 92%,
transparent
);
mask-image: linear-gradient(
90deg,
transparent,
#000 8%,
#000 92%,
transparent
);
} }
.marquee__route { .marquee__route {
@@ -1420,14 +1490,23 @@ dd {
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: -1; z-index: -1;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0.05) 40%, rgba(0, 0, 0, 0.65) 100%); background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.35) 0%,
rgba(0, 0, 0, 0.05) 40%,
rgba(0, 0, 0, 0.65) 100%
);
} }
.acard.is-active .acard__media::after, .acard.is-active .acard__media::after,
.acard:hover .acard__media::after, .acard:hover .acard__media::after,
.acard:focus-within .acard__media::after { .acard:focus-within .acard__media::after {
background: background:
radial-gradient(53.59% 137.34% at 50% 46.41%, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.4) 100%), radial-gradient(
53.59% 137.34% at 50% 46.41%,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.4) 100%
),
linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)),
linear-gradient(180deg, rgba(44, 175, 194, 0) 37.33%, #2cafc2 84.53%); linear-gradient(180deg, rgba(44, 175, 194, 0) 37.33%, #2cafc2 84.53%);
} }
@@ -1446,8 +1525,15 @@ dd {
box-shadow: none; box-shadow: none;
} }
.rail:hover .acard.is-active:not(:hover):not(:focus-within) .acard__media::after { .rail:hover
background: linear-gradient(180deg, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0.05) 40%, rgba(0, 0, 0, 0.65) 100%); .acard.is-active:not(:hover):not(:focus-within)
.acard__media::after {
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.35) 0%,
rgba(0, 0, 0, 0.05) 40%,
rgba(0, 0, 0, 0.65) 100%
);
} }
.acard__time { .acard__time {
@@ -1559,7 +1645,7 @@ dd {
gap: var(--sp-4); gap: var(--sp-4);
} }
.acc+.acc { .acc + .acc {
border-top: 1px solid var(--c-line); border-top: 1px solid var(--c-line);
padding-top: var(--sp-4); padding-top: var(--sp-4);
} }
@@ -1613,11 +1699,14 @@ dd {
transition: grid-template-rows 0.3s var(--ease); transition: grid-template-rows 0.3s var(--ease);
} }
.acc__q[aria-expanded="true"]+.acc__a { /* The button sits inside an <h3>, so .acc__a is the h3's sibling — not the
button's. Match on the .acc wrapper's expanded state instead of an adjacent
sibling, which never matched through the heading. */
.acc:has(.acc__q[aria-expanded="true"]) .acc__a {
grid-template-rows: 1fr; grid-template-rows: 1fr;
} }
.acc__a>* { .acc__a > * {
overflow: hidden; overflow: hidden;
} }
@@ -1634,7 +1723,8 @@ dd {
========================================================= */ ========================================================= */
.footer { .footer {
position: relative; position: relative;
margin: calc(var(--sp-6) + var(--page-inset)) var(--page-inset) var(--page-inset); margin: calc(var(--sp-6) + var(--page-inset)) var(--page-inset)
var(--page-inset);
padding-block: var(--sp-6) 0; padding-block: var(--sp-6) 0;
border-radius: var(--r-xl); border-radius: var(--r-xl);
color: var(--c-white); color: var(--c-white);
@@ -1770,7 +1860,11 @@ dd {
padding: var(--sp-4); padding: var(--sp-4);
padding-bottom: clamp(2rem, 1rem + 3vw, 3rem); padding-bottom: clamp(2rem, 1rem + 3vw, 3rem);
border-radius: var(--r-pill) var(--r-pill) 0 0; border-radius: var(--r-pill) var(--r-pill) 0 0;
background: linear-gradient(180deg, rgba(44, 175, 194, 0.99) 0%, var(--c-teal-20) 100%); background: linear-gradient(
180deg,
rgba(44, 175, 194, 0.99) 0%,
var(--c-teal-20) 100%
);
box-shadow: 0 0.75rem 8.25rem var(--c-teal); box-shadow: 0 0.75rem 8.25rem var(--c-teal);
-webkit-backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
@@ -1785,7 +1879,7 @@ dd {
Responsive Responsive
========================================================= */ ========================================================= */
@media (max-width: 78rem) { @media (max-width: 78rem) {
.rail--dest>* { .rail--dest > * {
flex: 0 0 calc((100% - 3 * var(--gap)) / 4); flex: 0 0 calc((100% - 3 * var(--gap)) / 4);
} }
} }
@@ -1813,7 +1907,7 @@ dd {
max-width: none; max-width: none;
} }
.rail--dest>* { .rail--dest > * {
flex: 0 0 calc((100% - 2 * var(--gap)) / 3); flex: 0 0 calc((100% - 2 * var(--gap)) / 3);
} }
@@ -1861,7 +1955,10 @@ dd {
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateY(-0.5rem); transform: translateY(-0.5rem);
transition: opacity 0.25s var(--ease), transform 0.25s var(--ease), visibility 0.25s; transition:
opacity 0.25s var(--ease),
transform 0.25s var(--ease),
visibility 0.25s;
} }
.nav.is-open { .nav.is-open {
@@ -1902,7 +1999,7 @@ dd {
display: none; display: none;
} }
.nav__toggle[aria-expanded="true"]+.nav__submenu { .nav__toggle[aria-expanded="true"] + .nav__submenu {
display: grid; display: grid;
} }
@@ -1915,7 +2012,7 @@ dd {
width: 100%; width: 100%;
} }
.rail--art>* { .rail--art > * {
flex: 0 0 calc((100% - var(--gap)) / 2); flex: 0 0 calc((100% - var(--gap)) / 2);
} }
@@ -1925,7 +2022,7 @@ dd {
} }
@media (max-width: 48rem) { @media (max-width: 48rem) {
.rail--dest>* { .rail--dest > * {
flex: 0 0 calc((100% - var(--gap)) / 2); flex: 0 0 calc((100% - var(--gap)) / 2);
} }
@@ -1953,10 +2050,6 @@ dd {
} }
@media (max-width: 40rem) { @media (max-width: 40rem) {
.hero {
min-height: clamp(30rem, 84svh, 44rem);
}
.booking { .booking {
padding: var(--sp-2); padding: var(--sp-2);
} }
@@ -1988,11 +2081,11 @@ dd {
flex: 1 1 auto; flex: 1 1 auto;
} }
.rail--dest>* { .rail--dest > * {
flex: 0 0 min(72%, 15rem); flex: 0 0 min(72%, 15rem);
} }
.rail--art>* { .rail--art > * {
flex: 0 0 min(85%, 22rem); flex: 0 0 min(85%, 22rem);
} }
@@ -2200,20 +2293,23 @@ dd {
border-radius: 0.25rem; border-radius: 0.25rem;
color: transparent; color: transparent;
background: var(--c-white); background: var(--c-white);
transition: color 0.2s var(--ease), background-color 0.2s var(--ease), border-color 0.2s var(--ease); transition:
color 0.2s var(--ease),
background-color 0.2s var(--ease),
border-color 0.2s var(--ease);
} }
.check:hover .check__box { .check:hover .check__box {
border-color: rgba(0, 0, 0, 0.45); border-color: rgba(0, 0, 0, 0.45);
} }
.check__input:checked+.check__box { .check__input:checked + .check__box {
color: var(--c-white); color: var(--c-white);
border-color: var(--c-black); border-color: var(--c-black);
background: var(--c-black); background: var(--c-black);
} }
.check__input:focus-visible+.check__box { .check__input:focus-visible + .check__box {
outline: 2px solid var(--c-teal); outline: 2px solid var(--c-teal);
outline-offset: 2px; outline-offset: 2px;
} }
@@ -2225,7 +2321,7 @@ dd {
transition: opacity 0.2s var(--ease); transition: opacity 0.2s var(--ease);
} }
.check__input:checked~.check__text { .check__input:checked ~ .check__text {
opacity: 1; opacity: 1;
} }
@@ -2345,7 +2441,9 @@ dd {
line-height: 1.5; line-height: 1.5;
text-align: justify; text-align: justify;
opacity: 0.7; opacity: 0.7;
transition: color 0.25s var(--ease), opacity 0.25s var(--ease); transition:
color 0.25s var(--ease),
opacity 0.25s var(--ease);
} }
.titem.is-active .titem__desc, .titem.is-active .titem__desc,
@@ -2357,7 +2455,9 @@ dd {
/* hovering any card in the grid hands the highlight over to it */ /* hovering any card in the grid hands the highlight over to it */
.tgrid:hover .titem.is-active:not(:hover):not(:focus-within) .titem__desc, .tgrid:hover .titem.is-active:not(:hover):not(:focus-within) .titem__desc,
.tgrid:focus-within .titem.is-active:not(:hover):not(:focus-within) .titem__desc { .tgrid:focus-within
.titem.is-active:not(:hover):not(:focus-within)
.titem__desc {
color: inherit; color: inherit;
opacity: 0.7; opacity: 0.7;
} }
@@ -2369,11 +2469,17 @@ dd {
.tgrid:hover .dcard.is-active:not(:hover):not(:focus-within)::after, .tgrid:hover .dcard.is-active:not(:hover):not(:focus-within)::after,
.tgrid:focus-within .dcard.is-active:not(:hover):not(:focus-within)::after { .tgrid:focus-within .dcard.is-active:not(:hover):not(:focus-within)::after {
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0.65) 100%); background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 35%,
rgba(0, 0, 0, 0.65) 100%
);
} }
.tgrid:hover .dcard.is-active:not(:hover):not(:focus-within) .ring-badge__dot, .tgrid:hover .dcard.is-active:not(:hover):not(:focus-within) .ring-badge__dot,
.tgrid:focus-within .dcard.is-active:not(:hover):not(:focus-within) .ring-badge__dot { .tgrid:focus-within
.dcard.is-active:not(:hover):not(:focus-within)
.ring-badge__dot {
color: var(--c-white); color: var(--c-white);
background: var(--c-glass); background: var(--c-glass);
} }
@@ -2411,7 +2517,10 @@ dd {
color: var(--c-black); color: var(--c-black);
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
transition: color 0.2s var(--ease), background-color 0.2s var(--ease), border-color 0.2s var(--ease); transition:
color 0.2s var(--ease),
background-color 0.2s var(--ease),
border-color 0.2s var(--ease);
} }
.pager__num { .pager__num {
@@ -2530,7 +2639,8 @@ dd {
--word: clamp(3rem, 0.4rem + 9.5vw, 11.25rem); --word: clamp(3rem, 0.4rem + 9.5vw, 11.25rem);
min-height: clamp(26rem, 62svh, 38rem); min-height: clamp(26rem, 62svh, 38rem);
margin: var(--page-inset); margin: var(--page-inset);
padding-block: calc(var(--header-h) + var(--sp-5)) clamp(0.75rem, 0.4rem + 1vw, 1.5rem); padding-block: calc(var(--header-h) + var(--sp-5))
clamp(0.75rem, 0.4rem + 1vw, 1.5rem);
border-radius: var(--r-xl); border-radius: var(--r-xl);
isolation: isolate; isolation: isolate;
overflow: clip; overflow: clip;
@@ -2547,6 +2657,13 @@ dd {
border-radius: inherit; border-radius: inherit;
object-fit: cover; object-fit: cover;
object-position: center 26%; object-position: center 26%;
/* Viator only ships ~720px images, which look soft blown up to a full-width
banner. Rather than fight it, lean in: a gentle blur turns the photo into an
intentional backdrop (the crisp image lives in the gallery lightbox). The
scale hides the blur's feathered edges, which overflow:clip on .hero-tour
then trims to the rounded frame. */
filter: blur(10px) saturate(1.12);
transform: scale(1.12);
} }
.hero-tour__sky { .hero-tour__sky {
@@ -2555,8 +2672,17 @@ dd {
z-index: -1; z-index: -1;
border-radius: inherit; border-radius: inherit;
background: background:
radial-gradient(70% 50% at 50% 100%, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0) 70%), radial-gradient(
linear-gradient(180deg, rgba(27, 147, 166, 0.82) 0%, rgba(44, 175, 194, 0.62) 45%, rgba(111, 208, 221, 0.78) 100%); 70% 50% at 50% 100%,
rgba(255, 255, 255, 0.3) 0%,
rgba(255, 255, 255, 0) 70%
),
linear-gradient(
180deg,
rgba(27, 147, 166, 0.55) 0%,
rgba(44, 175, 194, 0.42) 45%,
rgba(111, 208, 221, 0.72) 100%
);
} }
.hero-tour__inner { .hero-tour__inner {
@@ -2590,7 +2716,7 @@ dd {
font-weight: 500; font-weight: 500;
} }
.crumbs__list li+li::before { .crumbs__list li + li::before {
content: "/"; content: "/";
margin-inline-end: var(--sp-1); margin-inline-end: var(--sp-1);
opacity: 0.5; opacity: 0.5;
@@ -2681,7 +2807,7 @@ dd {
opacity: 0.7; opacity: 0.7;
} }
.tour__text+.tour__text { .tour__text + .tour__text {
margin-block-start: var(--sp-3); margin-block-start: var(--sp-3);
} }
@@ -2808,6 +2934,119 @@ dd {
border-radius: var(--r-card); border-radius: var(--r-card);
} }
.gallery img {
cursor: zoom-in;
transition: transform 0.25s var(--ease), filter 0.25s var(--ease);
}
.gallery img:hover {
filter: brightness(1.05);
}
/* =========================================================
Gallery lightbox (built by vc-design.js)
========================================================= */
.vc-lightbox {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: clamp(1rem, 0.5rem + 3vw, 3.5rem);
background: rgba(0, 0, 0, 0.86);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
opacity: 0;
visibility: hidden;
transition:
opacity 0.25s var(--ease),
visibility 0s linear 0.25s;
}
.vc-lightbox.is-open {
opacity: 1;
visibility: visible;
transition: opacity 0.25s var(--ease);
}
.vc-lightbox__img {
max-width: min(100%, 78rem);
max-height: 84vh;
width: auto;
height: auto;
border-radius: var(--r-card);
box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.5);
transform: scale(0.96);
transition: transform 0.25s var(--ease), opacity 0.15s var(--ease);
}
.vc-lightbox.is-open .vc-lightbox__img {
transform: scale(1);
}
.vc-lightbox__btn {
position: absolute;
display: grid;
place-items: center;
width: 3rem;
height: 3rem;
padding: 0;
border: 0;
border-radius: var(--r-pill);
color: var(--c-white);
background: rgba(255, 255, 255, 0.12);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
cursor: pointer;
transition: background 0.2s var(--ease), transform 0.2s var(--ease);
}
.vc-lightbox__btn:hover {
background: rgba(255, 255, 255, 0.24);
}
.vc-lightbox__close {
top: clamp(1rem, 3vw, 2rem);
right: clamp(1rem, 3vw, 2rem);
}
.vc-lightbox__prev,
.vc-lightbox__next {
top: 50%;
transform: translateY(-50%);
}
.vc-lightbox__prev {
left: clamp(0.5rem, 2vw, 2rem);
}
.vc-lightbox__next {
right: clamp(0.5rem, 2vw, 2rem);
}
.vc-lightbox__prev:hover,
.vc-lightbox__next:hover {
transform: translateY(-50%) scale(1.06);
}
.vc-lightbox__count {
position: absolute;
bottom: clamp(1rem, 3vw, 2rem);
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.85);
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.04em;
}
/* single image → hide the arrows */
.vc-lightbox--single .vc-lightbox__prev,
.vc-lightbox--single .vc-lightbox__next,
.vc-lightbox--single .vc-lightbox__count {
display: none;
}
/* --- included / not included --- */ /* --- included / not included --- */
.lists { .lists {
display: grid; display: grid;

Binary file not shown.

View File

@@ -16,23 +16,31 @@ listing/single/section assets load only on pages that use them.
| `[viator_top_tours]` | Rail of tour cards | `viator_tour` posts | | `[viator_top_tours]` | Rail of tour cards | `viator_tour` posts |
| `[viator_top_attractions]` | Rail of tour cards | `viator_tour` posts | | `[viator_top_attractions]` | Rail of tour cards | `viator_tour` posts |
| `[viator_browse_activities]` | Rail of tour cards | `viator_tour` posts | | `[viator_browse_activities]` | Rail of tour cards | `viator_tour` posts |
| `[viator_bus_tours]` | "Open Top Bus Tours" promo + 4-up card grid | `viator_tour` posts |
| `[viator_about]` | Static "About / Memorable" block + stat counters | Shortcode attributes |
| `[viator_articles]` | Rail of latest blog-post cards | `post` posts |
| `[viator_faq]` | FAQ accordion | Shortcode `items` / `vc_faq_items` filter | | `[viator_faq]` | FAQ accordion | Shortcode `items` / `vc_faq_items` filter |
| `[viator_testimonials]` | Testimonials marquee | `vc_testimonials` filter | | `[viator_testimonials]` | Testimonials marquee | `vc_testimonials` filter |
`[viator_hero_slider]` is kept as a **back-compat alias** for `[viator_hero]`. `[viator_hero_slider]` is kept as a **back-compat alias** for `[viator_hero]`.
A typical home page: Home page matching the design mockup (`new front/index.html`), top to bottom:
``` ```
[viator_hero] [viator_hero image_id="125304"]
[viator_top_destinations] [viator_top_destinations]
[viator_bus_tours image_id="125310"]
[viator_top_tours] [viator_top_tours]
[viator_top_attractions] [viator_about image_id="125311"]
[viator_browse_activities]
[viator_testimonials] [viator_testimonials]
[viator_articles]
[viator_faq] [viator_faq]
``` ```
Heavy decorative images (hero photo, open-top bus, isometric bus) are **not
bundled** — upload them in **Media** and pass the attachment `image_id` (exactly
like `[viator_hero image_id="…"]`). Icons and tiny decorations are inline SVG.
--- ---
## `[viator_hero]` ## `[viator_hero]`
@@ -99,6 +107,71 @@ are hidden automatically.
--- ---
## `[viator_bus_tours]`
The "Open Top Bus Tours…" promo block (route line, headline, "See all", a chip,
a bus illustration and note) followed by a 4-up grid of tour cards. Selects tours
with the **same attributes as the tour rails** (`ids` / `codes` / `destination` /
`tag` / `limit` / `orderby`), plus:
| Attribute | Default | Description |
|---|---|---|
| `title` | `Open Top<br>Bus Tours…` | Promo headline (`<br>` allowed). |
| `note` | *(default copy)* | Paragraph under the headline. |
| `chip` | `Very Fun!` | Small accent chip. |
| `image_id` | — | **Media attachment ID** of the open-top bus illustration. |
| `image` | — | …or a direct image URL (use `image_id` when possible). |
```
[viator_bus_tours image_id="125310" tag="hop-on-hop-off" limit="4"]
[viator_bus_tours ids="482,517,690,701"]
```
---
## `[viator_about]`
Static "About" marketing block: headline with an accent chip, a paragraph, four
stat counters and a decorative isometric bus. All copy is editable.
| Attribute | Default | Description |
|---|---|---|
| `title` | `Hop On Hop Off` | Headline. |
| `highlight` | `Memorable` | Accent chip appended to the headline. |
| `text` | *(default copy)* | Paragraph. |
| `button_text` | `About us` | Button label. |
| `button_url` | *(tours archive)* | Button link. |
| `image_id` | — | **Media attachment ID** of the isometric bus illustration. |
| `image` | — | …or a direct image URL. |
| `stats` | *(4 defaults)* | `25+|Years Experience;;500+|Tours Conducted` — `|` separates number from label, `;;` separates stats. A trailing `+` is styled. |
```
[viator_about image_id="125311" stats="25+|Years Experience;;500+|Tours Conducted;;430+|Tours this Year;;15+|Different Tours"]
```
---
## `[viator_articles]`
A rail of the latest blog posts styled as the design's "Latest Articles" cards
(featured image, relative time, title, excerpt → links to the post).
| Attribute | Default | Description |
|---|---|---|
| `title` | `Latest<br>Articles` | Heading (`<br>` allowed). |
| `lead` | *(default copy)* | Sub-heading. |
| `limit` | `8` | Number of posts. |
| `category` | — | Restrict to a category slug. |
| `view_all` | *(posts page)* | "See all" link; defaults to the blog page. |
Cards use each post's **featured image**; set one so the card isn't blank.
```
[viator_articles limit="8" category="travel-tips"]
```
---
## `[viator_faq]` ## `[viator_faq]`
FAQ accordion. FAQ accordion.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<svg class="sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg"> <svg class="sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0;overflow:hidden">
<defs> <defs>
<path id="ringPathScroll" fill="none" d="M50,50 m-39,0 a39,39 0 1,1 78,0 a39,39 0 1,1 -78,0" /> <path id="ringPathScroll" fill="none" d="M50,50 m-39,0 a39,39 0 1,1 78,0 a39,39 0 1,1 -78,0" />
</defs> </defs>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -92,4 +92,57 @@
var g = app.querySelector(".gallery, .tour-gallery"); var g = app.querySelector(".gallery, .tour-gallery");
if (g) g.scrollIntoView({ behavior: "smooth" }); if (g) g.scrollIntoView({ behavior: "smooth" });
}); });
/* ---------- gallery lightbox (zoom / view photos) ---------- */
(function () {
var imgs = Array.prototype.slice.call(app.querySelectorAll(".gallery img"));
if (!imgs.length) return;
var host = imgs[0].closest(".vc-app") || document.body;
var box = document.createElement("div");
box.className = "vc-lightbox" + (imgs.length < 2 ? " vc-lightbox--single" : "");
box.setAttribute("role", "dialog");
box.setAttribute("aria-modal", "true");
box.innerHTML =
'<button class="vc-lightbox__btn vc-lightbox__prev" type="button" aria-label="Previous"><svg class="icon icon--24" aria-hidden="true"><use href="#i-arrow-left"></use></svg></button>' +
'<img class="vc-lightbox__img" alt="">' +
'<button class="vc-lightbox__btn vc-lightbox__next" type="button" aria-label="Next"><svg class="icon icon--24" aria-hidden="true"><use href="#i-arrow-right"></use></svg></button>' +
'<button class="vc-lightbox__btn vc-lightbox__close" type="button" aria-label="Close"><svg class="icon icon--24" aria-hidden="true"><use href="#i-x"></use></svg></button>' +
'<span class="vc-lightbox__count"></span>';
host.appendChild(box);
var bigImg = box.querySelector(".vc-lightbox__img");
var count = box.querySelector(".vc-lightbox__count");
var idx = 0;
function show(i) {
idx = (i + imgs.length) % imgs.length;
bigImg.src = imgs[idx].currentSrc || imgs[idx].src;
bigImg.alt = imgs[idx].alt || "";
count.textContent = (idx + 1) + " / " + imgs.length;
}
function open(i) {
show(i);
box.classList.add("is-open");
document.documentElement.style.overflow = "hidden";
}
function close() {
box.classList.remove("is-open");
document.documentElement.style.overflow = "";
}
imgs.forEach(function (img, i) {
img.addEventListener("click", function () { open(i); });
});
box.querySelector(".vc-lightbox__prev").addEventListener("click", function () { show(idx - 1); });
box.querySelector(".vc-lightbox__next").addEventListener("click", function () { show(idx + 1); });
box.querySelector(".vc-lightbox__close").addEventListener("click", close);
box.addEventListener("click", function (e) { if (e.target === box) close(); });
document.addEventListener("keydown", function (e) {
if (!box.classList.contains("is-open")) return;
if (e.key === "Escape") close();
else if (e.key === "ArrowLeft") show(idx - 1);
else if (e.key === "ArrowRight") show(idx + 1);
});
})();
})(); })();

View File

@@ -17,7 +17,11 @@
'use strict'; 'use strict';
const fs = require('fs'); const fs = require('fs');
const SCOPE = process.env.VC_SCOPE || '.vc-app'; // Doubled class (`.vc-app.vc-app`) so every ported rule carries an extra class
// of specificity — enough to win against Astra's content selectors (e.g.
// `.ast-… .entry-content a`) without touching the markup, which still only
// needs a single `class="vc-app"`. Override with VC_SCOPE if ever needed.
const SCOPE = process.env.VC_SCOPE || '.vc-app.vc-app';
function prefixSelector(sel) { function prefixSelector(sel) {
sel = sel.trim(); sel = sel.trim();

View File

@@ -11,6 +11,9 @@ class VC_Display {
'viator_top_destinations', 'viator_top_destinations',
'viator_top_attractions', 'viator_top_attractions',
'viator_browse_activities', 'viator_browse_activities',
'viator_bus_tours',
'viator_about',
'viator_articles',
'viator_faq', 'viator_faq',
'viator_testimonials', 'viator_testimonials',
'viator_tour_card', 'viator_tour_card',
@@ -24,9 +27,12 @@ class VC_Display {
); );
public static function init() { public static function init() {
// Priority 20 so Astra has already registered/enqueued its stylesheet; // Priority 100 so the theme (Astra + child) has already enqueued ALL of
// our CSS then loads after it and wins equal-specificity ties. // its stylesheets — including the parent style.css and the child's chrome
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ), 20 ); // CSS, which are enqueued by the theme at priority 20 and would otherwise
// print AFTER us (plugins run before themes at equal priority) and win
// equal-specificity ties. Loading last makes our design win those ties.
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ), 100 );
add_action( 'template_redirect', array( __CLASS__, 'maybe_enrich_single' ) ); add_action( 'template_redirect', array( __CLASS__, 'maybe_enrich_single' ) );
add_action( 'pre_get_posts', array( __CLASS__, 'filter_archive_query' ) ); add_action( 'pre_get_posts', array( __CLASS__, 'filter_archive_query' ) );
add_filter( 'single_template', array( __CLASS__, 'single_template' ) ); add_filter( 'single_template', array( __CLASS__, 'single_template' ) );
@@ -146,11 +152,15 @@ class VC_Display {
wp_enqueue_style( 'vc-fonts', 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;700;800&display=swap', array(), null ); wp_enqueue_style( 'vc-fonts', 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;700;800&display=swap', array(), null );
// One scoped stylesheet (.vc-app) covers listing, single, and homepage // One scoped stylesheet (.vc-app) covers listing, single, and homepage
// sections — ported from the "Hop On Hop Off" design. Depend on the Astra // sections — ported from the "Hop On Hop Off" design. Declare every theme
// theme stylesheet (when present) so our rules load AFTER it and win ties. // stylesheet that could fight ours as a dependency (when present) so our
// rules always print AFTER them and win equal-specificity ties. We enqueue
// at priority 100, so by now the theme (priority 20) has registered them.
$deps = array( 'vc-fonts' ); $deps = array( 'vc-fonts' );
if ( wp_style_is( 'astra-theme-css', 'registered' ) ) { foreach ( array( 'astra-theme-css', 'astra-parent', 'hoho-chrome' ) as $handle ) {
$deps[] = 'astra-theme-css'; if ( wp_style_is( $handle, 'registered' ) ) {
$deps[] = $handle;
}
} }
wp_enqueue_style( 'vc-design', VC_PLUGIN_URL . 'assets/css/vc-design.css', $deps, VC_VERSION ); wp_enqueue_style( 'vc-design', VC_PLUGIN_URL . 'assets/css/vc-design.css', $deps, VC_VERSION );
wp_add_inline_style( 'vc-design', self::astra_overrides() ); wp_add_inline_style( 'vc-design', self::astra_overrides() );
@@ -180,42 +190,89 @@ class VC_Display {
. '.vc-fullwidth.ast-page-builder-template .site-main>article' . '.vc-fullwidth.ast-page-builder-template .site-main>article'
. '{padding:0!important;margin:0!important;border:0!important;max-width:none!important;}', . '{padding:0!important;margin:0!important;border:0!important;max-width:none!important;}',
'.vc-fullwidth #primary,.vc-fullwidth #content{margin:0!important;padding:0!important;}', '.vc-fullwidth #primary,.vc-fullwidth #content{margin:0!important;padding:0!important;}',
// display:block undoes Astra's `.site-content .ast-container{display:flex}`
// (≥922px), which otherwise sizes our content to its contents instead
// of filling the full-width container.
'.vc-fullwidth .site-content .ast-container,.vc-fullwidth .ast-container' '.vc-fullwidth .site-content .ast-container,.vc-fullwidth .ast-container'
. '{max-width:none!important;padding-left:0!important;padding-right:0!important;}', . '{max-width:none!important;padding-left:0!important;padding-right:0!important;display:block!important;}',
'.vc-fullwidth .entry-content{margin-block:0!important;}', '.vc-fullwidth .entry-content{margin-block:0!important;}',
'.vc-fullwidth .site-content{padding:0!important;}', '.vc-fullwidth .site-content{padding:0!important;}',
// Guard against a stray horizontal scrollbar from any full-bleed child. // Guard against a stray horizontal scrollbar from any full-bleed child.
'body.vc-fullwidth{overflow-x:hidden;}', 'body.vc-fullwidth{overflow-x:hidden;}',
// A gradient stands in when no hero image is set, so the banner is // A gradient stands in when no hero image is set, so the banner is
// never blank. // never blank.
'.vc-app .hero__media,.vc-app .hero-tour{background:linear-gradient(120deg,#101317,#2cafc2);}', '.vc-app.vc-app .hero__media,.vc-app.vc-app .hero-tour{background:linear-gradient(120deg,#101317,#2cafc2);}',
// Normalize: stop Astra from decorating our components. Its // Normalize: stop Astra from decorating our components. Its
// .entry-content rules (underlined links, list bullets/indent) // .entry-content rules (underlined links, list bullets/indent)
// out-specify our scoped base, so reset them here (!important beats // out-specify our scoped base, so reset them here (!important beats
// e.g. `.ast-single-post .entry-content a`). The .vc-app.vc-app // e.g. `.ast-single-post .entry-content a`). Every selector matches
// doubling lifts specificity without touching design class rules. // the doubled `.vc-app.vc-app` scope of the main stylesheet so these
'.vc-app a,.vc-app.vc-app a{text-decoration:none;box-shadow:none;}', // patches keep winning against it, not just against the theme.
'.vc-app :is(ul,ol),.vc-app.vc-app :is(ul,ol){list-style:none!important;margin:0!important;padding:0!important;}', '.vc-app.vc-app a{text-decoration:none;box-shadow:none;}',
'.vc-app li{margin:0!important;}', // No !important on margin/padding: the doubled .vc-app.vc-app scope
// already out-specifies Astra's list rules, and !important here was
// steamrolling design lists that need their own spacing — e.g. the
// rail (<ul class="rail">) whose vertical padding gives the hover glow
// room. list-style stays !important (purely cosmetic; prose lists
// re-enable bullets via a higher-specificity rule).
'.vc-app.vc-app :is(ul,ol){list-style:none!important;margin:0;padding:0;}',
'.vc-app.vc-app li{margin:0;}',
// …but keep the tour "Overview" prose (the_content) readable. // …but keep the tour "Overview" prose (the_content) readable.
'.vc-app .tour__text a{text-decoration:underline;}', '.vc-app.vc-app .tour__text a{text-decoration:underline;}',
'.vc-app .tour__text ul{list-style:disc!important;margin:1em 0!important;padding-inline-start:1.5em!important;}', '.vc-app.vc-app .tour__text ul{list-style:disc!important;margin:1em 0!important;padding-inline-start:1.5em!important;}',
'.vc-app .tour__text ol{list-style:decimal!important;margin:1em 0!important;padding-inline-start:1.5em!important;}', '.vc-app.vc-app .tour__text ol{list-style:decimal!important;margin:1em 0!important;padding-inline-start:1.5em!important;}',
'.vc-app .tour__text li{margin:.25em 0!important;}', '.vc-app.vc-app .tour__text li{margin:.25em 0!important;}',
// Links must inherit the design colour (Astra colours links with its // Links must inherit the design colour (Astra colours links with its
// theme colour, which turned white card titles/destination names into // theme colour, which turned white card titles/destination names into
// coloured text). Buttons keep their own colour. // coloured text). Buttons keep their own colour.
'.vc-app a:not(.btn){color:inherit;}', // …but not ring-badges: they set their own colour on the <a>, and this
'.vc-app .tour__text a{color:var(--c-teal);}', // inherit rule out-specifies .ring-badge--top/--scroll, which turned
// the badge text white (inherited) on light backgrounds.
'.vc-app.vc-app a:not(.btn):not(.ring-badge){color:inherit;}',
'.vc-app.vc-app .tour__text a{color:var(--c-teal);}',
// Real tour titles are far longer than the mockup\'s city names — clamp // Real tour titles are far longer than the mockup\'s city names — clamp
// them and stop price/duration values wrapping mid-number. // them and stop price/duration values wrapping mid-number.
'.vc-app .dcard__title{display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}', '.vc-app.vc-app .dcard__title{display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}',
'.vc-app .titem__desc{display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}', '.vc-app.vc-app .titem__desc{display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}',
'.vc-app .dcard__meta .meta__v{white-space:nowrap;}', // Real tour descriptions run long; the on-hover card blurb clamps to
// three lines with an ellipsis instead of spilling over the card.
'.vc-app.vc-app .dcard__desc p{display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;}',
'.vc-app.vc-app .dcard__meta .meta__v{white-space:nowrap;}',
// Astra's page-builder layout centres/limits every entry-content child
// (max-width:none; margin:auto), which flattened the hero inset. Our
// sections are full-bleed, so re-assert 0 side-margins; !important
// beats Astra's 4-class selector. The hero keeps its small inset — the
// only section with an intentional side margin.
'.vc-app.vc-app>*{max-width:none!important;margin-inline:0!important;}',
// Heroes keep their small inset (rounded card look) — .hero on the
// home/archive pages and .hero-tour on the single-tour page. Without
// this they went full-bleed after the margin-inline:0 reset above.
'.vc-app.vc-app>.hero,.vc-app.vc-app>.hero-tour{margin:var(--page-inset)!important;}',
// Let vertical decorations (rotating badges, card glow, promo bus, FAQ
// watermark) escape, but keep the horizontal axis clipped so a
// full-bleed section or decorative element can't cause a mobile
// sideways scroll. overflow-x:clip (unlike hidden) does NOT force
// overflow-y to auto, so the y-axis stays truly visible.
'.vc-app.vc-app{overflow-x:clip;overflow-y:visible;}',
// Astra wraps the content in article/.entry-content/.ast-container,
// some of which clip overflow — that cut the card hover glow at the
// rail edges. Open them up on our pages only (page scroll stays
// contained by body.vc-fullwidth{overflow-x:hidden}).
'.vc-fullwidth .site-main,.vc-fullwidth #primary,.vc-fullwidth #content,'
. '.vc-fullwidth .site-content,.vc-fullwidth .ast-container,'
. '.vc-fullwidth .entry-content,.vc-fullwidth .site-main>article'
. '{overflow:visible!important;}',
// FAQ question buttons must stay flat — Astra gives <button> a shadow.
'.vc-app.vc-app .acc__q{box-shadow:none!important;}',
// Astra colours every heading on the element itself (h1–h6 and
// .entry-content :where(h1..h6)), beating our container-inherited
// colours. Make headings inherit again — no !important, so design
// classes (.hero__title, the white .tcard titles) keep their own.
'.vc-app.vc-app :is(h1,h2,h3,h4,h5,h6){color:inherit;}',
// Typography: force the design font over Astra inside our scopes. // Typography: force the design font over Astra inside our scopes.
'.vc-app,.vc-app h1,.vc-app h2,.vc-app h3,.vc-app h4,.vc-app h5,.vc-app h6,' '.vc-app.vc-app,.vc-app.vc-app h1,.vc-app.vc-app h2,.vc-app.vc-app h3,.vc-app.vc-app h4,.vc-app.vc-app h5,.vc-app.vc-app h6,'
. '.vc-app p,.vc-app a,.vc-app span,.vc-app li,.vc-app button,.vc-app input,' . '.vc-app.vc-app p,.vc-app.vc-app a,.vc-app.vc-app span,.vc-app.vc-app li,.vc-app.vc-app button,.vc-app.vc-app input,'
. '.vc-app select,.vc-app textarea,.vc-app blockquote' . '.vc-app.vc-app select,.vc-app.vc-app textarea,.vc-app.vc-app blockquote'
. '{font-family:"Plus Jakarta Sans",system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}', . '{font-family:"Plus Jakarta Sans",system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}',
) )
); );

View File

@@ -15,6 +15,9 @@ class VC_Sections {
const TOP_ATTRS = 'viator_top_attractions'; const TOP_ATTRS = 'viator_top_attractions';
const ACTIVITIES = 'viator_browse_activities'; const ACTIVITIES = 'viator_browse_activities';
const TOP_TOURS = 'viator_top_tours'; const TOP_TOURS = 'viator_top_tours';
const BUS_TOURS = 'viator_bus_tours';
const ABOUT = 'viator_about';
const ARTICLES = 'viator_articles';
const FAQ = 'viator_faq'; const FAQ = 'viator_faq';
const REVIEWS = 'viator_testimonials'; const REVIEWS = 'viator_testimonials';
@@ -28,6 +31,9 @@ class VC_Sections {
add_shortcode( self::TOP_ATTRS, array( __CLASS__, 'top_attractions' ) ); add_shortcode( self::TOP_ATTRS, array( __CLASS__, 'top_attractions' ) );
add_shortcode( self::ACTIVITIES, array( __CLASS__, 'browse_activities' ) ); add_shortcode( self::ACTIVITIES, array( __CLASS__, 'browse_activities' ) );
add_shortcode( self::TOP_TOURS, array( __CLASS__, 'top_tours' ) ); add_shortcode( self::TOP_TOURS, array( __CLASS__, 'top_tours' ) );
add_shortcode( self::BUS_TOURS, array( __CLASS__, 'bus_tours' ) );
add_shortcode( self::ABOUT, array( __CLASS__, 'about' ) );
add_shortcode( self::ARTICLES, array( __CLASS__, 'articles' ) );
add_shortcode( self::FAQ, array( __CLASS__, 'faq' ) ); add_shortcode( self::FAQ, array( __CLASS__, 'faq' ) );
add_shortcode( self::REVIEWS, array( __CLASS__, 'testimonials' ) ); add_shortcode( self::REVIEWS, array( __CLASS__, 'testimonials' ) );
} }
@@ -130,7 +136,7 @@ class VC_Sections {
* Open a titled rail section and return its unique rail id. Caller fills the * Open a titled rail section and return its unique rail id. Caller fills the
* <ul> with cards and then calls close_rail(). * <ul> with cards and then calls close_rail().
*/ */
private static function open_rail( $title, $lead, $view_all_url, $view_all_text ) { private static function open_rail( $title, $lead, $view_all_url, $view_all_text, $rail_class = 'rail--dest' ) {
$id = 'vcRail' . ( ++self::$rail_seq ); $id = 'vcRail' . ( ++self::$rail_seq );
ob_start(); ob_start();
?> ?>
@@ -150,7 +156,7 @@ class VC_Sections {
</div> </div>
</div> </div>
</div> </div>
<ul class="rail rail--dest" id="<?php echo esc_attr( $id ); ?>"> <ul class="rail <?php echo esc_attr( $rail_class ); ?>" id="<?php echo esc_attr( $id ); ?>">
<?php <?php
return array( 'id' => $id, 'head' => ob_get_clean() ); return array( 'id' => $id, 'head' => ob_get_clean() );
} }
@@ -450,6 +456,299 @@ class VC_Sections {
return ob_get_clean(); return ob_get_clean();
} }
/**
* [viator_bus_tours limit="4" tag="hop-on-hop-off" image_id="123"]
*
* The "Open Top Bus Tours…" promo block + a 4-up grid of tour cards. Backed
* by the same curated/filtered tour query as the rails. The bus illustration
* is a heavy image, so it comes from the Media Library via image_id (or a
* direct image URL) — nothing is bundled with the plugin.
*/
public static function bus_tours( $atts ) {
$atts = shortcode_atts(
array(
'ids' => '',
'codes' => '',
'destination' => '',
'tag' => '',
'limit' => 4,
'orderby' => 'rating',
'title' => __( 'Open Top<br>Bus Tours&hellip;', 'viator-collector' ),
'note' => __( 'An open-top hop on hop off bus is the best way to discover the historic and cultural sights of any city.', 'viator-collector' ),
'chip' => __( 'Very Fun!', 'viator-collector' ),
'image_id' => '', // WordPress media attachment ID for the bus illustration.
'image' => '', // …or a direct image URL.
),
$atts,
self::BUS_TOURS
);
$query = self::tour_query( $atts );
if ( ! $query->have_posts() ) {
return '';
}
$archive = get_post_type_archive_link( 'viator_tour' );
$archive = $archive ? $archive : home_url( '/' );
$bus_img = '';
if ( '' !== $atts['image_id'] ) {
// 'full' (uncropped original) — some sites register a hard-cropped
// square 'large', which slices decorative illustrations in half.
$bus_img = wp_get_attachment_image( absint( $atts['image_id'] ), 'full', false, array( 'alt' => '', 'loading' => 'lazy' ) );
} elseif ( '' !== $atts['image'] ) {
$bus_img = '<img src="' . esc_url( $atts['image'] ) . '" alt="" loading="lazy">';
}
ob_start();
?>
<div class="vc-app vc-sections">
<section class="section section--tight">
<div class="container bus">
<div class="promo">
<svg class="promo__route" viewBox="0 0 416 526" fill="none" aria-hidden="true" preserveAspectRatio="none">
<path d="M-20 470 C 90 470, 60 350, 150 330 S 300 300, 300 210 S 210 120, 300 60 S 430 40, 440 -10" stroke="#2CAFC2" stroke-dasharray="3 3" />
</svg>
<div class="promo__head">
<h2 class="promo__title"><?php echo wp_kses( $atts['title'], array( 'br' => array() ) ); ?></h2>
<a class="btn btn--white" href="<?php echo esc_url( $archive ); ?>"><span><?php echo esc_html__( 'See all', 'viator-collector' ); ?></span><?php echo vc_icon( 'arrow-ne' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></a>
</div>
<?php if ( $atts['chip'] ) : ?><span class="chip chip--accent promo__chip"><?php echo esc_html( $atts['chip'] ); ?></span><?php endif; ?>
<?php if ( $bus_img ) : ?><div class="promo__bus" aria-hidden="true"><?php echo $bus_img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div><?php endif; ?>
<?php if ( $atts['note'] ) : ?><p class="promo__note"><?php echo esc_html( $atts['note'] ); ?></p><?php endif; ?>
</div>
<ul class="bus__grid">
<?php
$i = 0;
while ( $query->have_posts() ) :
$query->the_post();
++$i;
$pid = get_the_ID();
$price = get_post_meta( $pid, '_vc_price_from', true );
$currency = get_post_meta( $pid, '_vc_currency', true );
$terms = get_the_terms( $pid, 'viator_destination' );
$kicker = ( $terms && ! is_wp_error( $terms ) ) ? $terms[0]->name : '';
$active = ( 1 === $i );
$badge = vc_ring_badge( 'BOOK NOW · BOOK NOW · ', get_permalink( $pid ), 'card' );
if ( $active ) {
$badge = str_replace( 'ring-badge--card', 'ring-badge--card ring-badge--accent', $badge );
}
?>
<li class="tcard<?php echo $active ? ' is-active' : ''; ?>">
<?php echo vc_tour_image_html( $pid, 'tcard__img' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<div class="tcard__top">
<?php if ( $kicker ) : ?><span class="tcard__k"><?php echo esc_html( $kicker ); ?></span><?php endif; ?>
<h3 class="tcard__title"><a href="<?php echo esc_url( get_permalink( $pid ) ); ?>"><?php echo esc_html( get_the_title( $pid ) ); ?></a></h3>
</div>
<div class="tcard__bottom">
<?php if ( '' !== (string) $price ) : ?>
<div class="meta">
<span class="meta__k"><?php echo esc_html__( 'Start at', 'viator-collector' ); ?></span>
<span class="meta__v"><?php echo esc_html( vc_money( $price, $currency ) ); ?></span>
</div>
<?php endif; ?>
<?php echo $badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
</li>
<?php endwhile; ?>
</ul>
</div>
</section>
</div>
<?php
wp_reset_postdata();
return ob_get_clean();
}
/**
* [viator_about image_id="123" title="Hop On Hop Off" highlight="Memorable"
* stats="25+|Years Experience;;500+|Tours Conducted"]
*
* A static "About" marketing block (headline + chip, paragraph, stat counters
* and a decorative bus). All copy is editable via attributes; the isometric
* bus is a heavy image supplied from the Media Library (image_id / image).
*/
public static function about( $atts ) {
$atts = shortcode_atts(
array(
'title' => __( 'Hop On Hop Off', 'viator-collector' ),
'highlight' => __( 'Memorable', 'viator-collector' ),
'text' => __( 'Discover cities the memorable way — hop on and off our open-top buses at every major sight and explore each place at your own pace, with expert local commentary along the way.', 'viator-collector' ),
'button_text' => __( 'About us', 'viator-collector' ),
'button_url' => '',
'image_id' => '',
'image' => '',
'stats' => '', // "25+|Years Experience;;500+|Tours Conducted;;430+|Tours this Year;;15+|Different Tours"
),
$atts,
self::ABOUT
);
$stats = array();
if ( '' !== $atts['stats'] ) {
foreach ( explode( ';;', $atts['stats'] ) as $pair ) {
$p = array_map( 'trim', explode( '|', $pair, 2 ) );
if ( '' !== $p[0] && isset( $p[1] ) ) {
$stats[] = array( 'num' => $p[0], 'label' => $p[1] );
}
}
}
if ( empty( $stats ) ) {
$stats = array(
array( 'num' => '25+', 'label' => __( 'Years Experience', 'viator-collector' ) ),
array( 'num' => '500+', 'label' => __( 'Tours Conducted', 'viator-collector' ) ),
array( 'num' => '430+', 'label' => __( 'Tours this Year', 'viator-collector' ) ),
array( 'num' => '15+', 'label' => __( 'Different Tours', 'viator-collector' ) ),
);
}
$button_url = $atts['button_url'];
if ( '' === $button_url ) {
$button_url = get_post_type_archive_link( 'viator_tour' );
$button_url = $button_url ? $button_url : home_url( '/' );
}
$bus_img = '';
if ( '' !== $atts['image_id'] ) {
// 'full' (uncropped original) — some sites register a hard-cropped
// square 'large', which slices decorative illustrations in half.
$bus_img = wp_get_attachment_image( absint( $atts['image_id'] ), 'full', false, array( 'alt' => '', 'loading' => 'lazy' ) );
} elseif ( '' !== $atts['image'] ) {
$bus_img = '<img src="' . esc_url( $atts['image'] ) . '" alt="" loading="lazy">';
}
ob_start();
?>
<div class="vc-app vc-sections">
<section class="section">
<div class="container about">
<div class="about__art" aria-hidden="true">
<span class="about__glow"></span>
<span class="about__trail" aria-hidden="true"></span>
<?php if ( $bus_img ) : ?><span class="about__bus"><?php echo $bus_img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span><?php endif; ?>
<svg class="about__arcs" viewBox="0 0 480 440" fill="none" aria-hidden="true">
<defs>
<path id="busArc" pathLength="100" d="M20 336.6V221.6C20 96.6 170 23.4 170 148.4V263.4" />
</defs>
<g stroke="#FFCC00" stroke-width="9" stroke-linecap="round" fill="none" transform="translate(16 24)">
<use href="#busArc" opacity="0.1" stroke-dasharray="62 100" />
<use href="#busArc" opacity="0.25" stroke-dasharray="69 100" transform="translate(48 22)" />
<use href="#busArc" opacity="0.45" stroke-dasharray="76 100" transform="translate(96 44)" />
<use href="#busArc" opacity="0.8" stroke-dasharray="83 100" transform="translate(144 66)" />
</g>
</svg>
</div>
<div class="about__body">
<div class="about__head">
<h2 class="about__title"><?php echo esc_html( $atts['title'] ); ?><?php if ( $atts['highlight'] ) : ?><span class="chip chip--accent about__chip"><?php echo esc_html( $atts['highlight'] ); ?></span><?php endif; ?></h2>
<a class="btn btn--dark" href="<?php echo esc_url( $button_url ); ?>"><span><?php echo esc_html( $atts['button_text'] ); ?></span><?php echo vc_icon( 'arrow-right' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></a>
</div>
<?php if ( $atts['text'] ) : ?><p class="about__text"><?php echo esc_html( $atts['text'] ); ?></p><?php endif; ?>
<dl class="stats">
<?php
foreach ( $stats as $s ) :
$num = $s['num'];
$suffix = '';
if ( preg_match( '/^(.*?)(\+)$/', $num, $m ) ) {
$num = $m[1];
$suffix = $m[2];
}
?>
<div class="stat">
<dt class="stat__num"><?php echo esc_html( $num ); ?><?php if ( $suffix ) : ?><span><?php echo esc_html( $suffix ); ?></span><?php endif; ?></dt>
<dd class="stat__label"><?php echo esc_html( $s['label'] ); ?></dd>
</div>
<?php endforeach; ?>
</dl>
</div>
</div>
</section>
</div>
<?php
return ob_get_clean();
}
/**
* [viator_articles limit="8" category="news"]
*
* A rail of the latest blog posts, styled as the design's "Latest Articles"
* cards. Uses each post's featured image, time and excerpt; links to the post.
*/
public static function articles( $atts ) {
$atts = shortcode_atts(
array(
'title' => __( 'Latest<br>Articles', 'viator-collector' ),
'lead' => __( 'News, tips and stories from our tours.', 'viator-collector' ),
'limit' => 8,
'category' => '',
'view_all' => '',
),
$atts,
self::ARTICLES
);
$args = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => max( 1, absint( $atts['limit'] ) ),
'ignore_sticky_posts' => true,
'no_found_rows' => true,
);
if ( '' !== $atts['category'] ) {
$args['category_name'] = sanitize_title( $atts['category'] );
}
$query = new WP_Query( $args );
if ( ! $query->have_posts() ) {
return '';
}
$view_all = $atts['view_all'];
if ( '' === $view_all ) {
$posts_page = (int) get_option( 'page_for_posts' );
$view_all = $posts_page ? get_permalink( $posts_page ) : home_url( '/' );
}
$rail = self::open_rail( $atts['title'], $atts['lead'], $view_all, __( 'See all', 'viator-collector' ), 'rail--art' );
ob_start();
echo $rail['head']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$i = 0;
while ( $query->have_posts() ) :
$query->the_post();
++$i;
$pid = get_the_ID();
$active = ( 1 === $i );
$img = has_post_thumbnail( $pid ) ? get_the_post_thumbnail( $pid, 'large', array( 'class' => 'acard__img', 'loading' => 'lazy', 'alt' => '' ) ) : '';
$time = human_time_diff( (int) get_post_time( 'U', true, $pid ), (int) current_time( 'timestamp', true ) );
$excerpt = get_the_excerpt( $pid );
$badge = vc_ring_badge( 'READ MORE · READ MORE · ', get_permalink( $pid ), 'card' );
if ( $active ) {
$badge = str_replace( 'ring-badge--card', 'ring-badge--card ring-badge--accent', $badge );
}
?>
<li class="acard<?php echo $active ? ' is-active' : ''; ?>">
<div class="acard__media">
<?php echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<span class="chip chip--glass acard__time"><?php printf( /* translators: %s: human-readable time difference, e.g. "3 hours". */ esc_html__( '%s ago', 'viator-collector' ), esc_html( $time ) ); ?></span>
<div class="acard__overlay">
<h3 class="acard__title"><a href="<?php echo esc_url( get_permalink( $pid ) ); ?>"><?php echo esc_html( get_the_title( $pid ) ); ?></a></h3>
<?php echo $badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
</div>
<?php if ( $excerpt ) : ?><p class="acard__excerpt"><?php echo esc_html( $excerpt ); ?></p><?php endif; ?>
</li>
<?php
endwhile;
echo self::close_rail(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
wp_reset_postdata();
return ob_get_clean();
}
/** /**
* Shared tour query for the landing sections. Supports curation (ids / * Shared tour query for the landing sections. Supports curation (ids /
* product codes) and filtering (destination / tag), with a sane fallback. * product codes) and filtering (destination / tag), with a sane fallback.

View File

@@ -595,7 +595,10 @@ function vc_print_icon_sprite() {
* e.g. vc_icon( 'star', 'rating__star' ) => <svg class="icon rating__star">…</svg> * e.g. vc_icon( 'star', 'rating__star' ) => <svg class="icon rating__star">…</svg>
*/ */
function vc_icon( $name, $extra_class = '' ) { function vc_icon( $name, $extra_class = '' ) {
$name = preg_replace( '/[^a-z0-9\-]/', '', 'i-' . ltrim( (string) $name, 'i-' ) ); // Strip an existing "i-" prefix (once), then re-add it. ltrim() with a
// charlist would eat any leading i/-, turning "instagram" into "nstagram".
$slug = preg_replace( '/^i-/', '', (string) $name );
$name = preg_replace( '/[^a-z0-9\-]/', '', 'i-' . $slug );
$class = 'icon' . ( $extra_class ? ' ' . $extra_class : '' ); $class = 'icon' . ( $extra_class ? ' ' . $extra_class : '' );
return '<svg class="' . esc_attr( $class ) . '" aria-hidden="true"><use href="#' . esc_attr( $name ) . '"></use></svg>'; return '<svg class="' . esc_attr( $class ) . '" aria-hidden="true"><use href="#' . esc_attr( $name ) . '"></use></svg>';
} }

View File

@@ -38,9 +38,18 @@ while ( have_posts() ) :
?> ?>
<div class="vc-app vc-single"> <div class="vc-app vc-single">
<section class="hero-tour"> <section class="hero-tour">
<?php if ( $primary ) : ?> <?php
// Prefer a WordPress featured image (full size, responsive srcset) for a
// sharp hero — Viator's own image URLs cap out around 720px and look soft
// blown up to a full-width banner. Fall back to the Viator primary image.
if ( has_post_thumbnail( $post_id ) ) :
echo get_the_post_thumbnail( $post_id, 'full', array( 'class' => 'hero-tour__photo', 'fetchpriority' => 'high', 'alt' => '', 'sizes' => '100vw' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
elseif ( $primary ) :
?>
<img class="hero-tour__photo" src="<?php echo esc_url( $primary ); ?>" alt="" fetchpriority="high"> <img class="hero-tour__photo" src="<?php echo esc_url( $primary ); ?>" alt="" fetchpriority="high">
<?php endif; ?> <?php
endif;
?>
<div class="hero-tour__sky" aria-hidden="true"></div> <div class="hero-tour__sky" aria-hidden="true"></div>
<div class="container"> <div class="container">
<div class="hero-tour__block"> <div class="hero-tour__block">

View File

@@ -4,13 +4,13 @@ defined('ABSPATH') || exit;
/** /**
* Plugin Name: Viator Collector * Plugin Name: Viator Collector
* Description: Import Viator tours using the Viator Partner API and display them with native WordPress post types, templates, and shortcodes. * Description: Import Viator tours using the Viator Partner API and display them with native WordPress post types, templates, and shortcodes.
* Version: 1.8.4 * Version: 1.8.15
* Author: Amir Bagheri * Author: Amir Bagheri
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
* Text Domain: viator-collector * Text Domain: viator-collector
*/ */
define('VC_VERSION', '1.8.4'); define('VC_VERSION', '1.8.15');
define('VC_PLUGIN_FILE', __FILE__); define('VC_PLUGIN_FILE', __FILE__);
define('VC_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('VC_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('VC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('VC_PLUGIN_URL', plugin_dir_url(__FILE__));