// Shared, page-agnostic section components for The Vivaan Hotel & Resorts. // Built on top of window.VivaanDesignSystem_b1c8ba (Hero, Button, NavigationBar, // BookingPanel, FormField). These are page-specific compositions, not formal // DS components — same pattern as DiscoverSection/Footer in the DS's own // ui_kit reference. const { Button } = window.VivaanDesignSystem_b1c8ba; /* ---------------------------------------------------------------------- */ /* Primitives */ /* ---------------------------------------------------------------------- */ // Tasteful abstract placeholder standing in for estate photography — the DS // itself falls back to flat tone fields rather than fabricated photos, so we // follow that convention throughout instead of pulling in stock imagery. function PhotoBlock({ tone = "taupe", caption, ratio, style }) { // Flat single-token fields only — matches Hero's own no-photo fallback // (var(--vivaan-taupe), no gradient) and the DS's "no decorative // gradients" rule. A photo would replace this entirely; this is a stand-in. const tones = { taupe: "var(--vivaan-taupe)", cream: "var(--vivaan-cream)", ink: "var(--vivaan-black)", gold: "var(--vivaan-gold)", }; const { backgroundImage, ...restStyle } = style || {}; const wrap = { position: "relative", aspectRatio: ratio || "4 / 5", backgroundColor: tones[tone], overflow: "hidden", ...restStyle, }; const cap = { position: "absolute", left: 20, bottom: 18, right: 20, color: tone === "cream" ? "var(--color-text-primary)" : "var(--color-text-on-dark)", fontFamily: "var(--font-serif)", fontSize: 11, letterSpacing: "0.18em", textTransform: "uppercase", opacity: 0.9, }; return (
{caption ?
{caption}
: null}
); } // Hover-swap: shows images[0] by default, crossfades to images[1] on // hover/focus (and back on leave). Used for room cards with a "before/after" // or two-angle photo pair. Falls back to a flat PhotoBlock if no images. function HoverImageSwap({ images, tone, ratio, caption, style, fadeMs = 500 }) { const [hovered, setHovered] = React.useState(false); if (!images || images.length < 2) { return ; } const wrap2 = { position: "relative", aspectRatio: ratio || "4 / 5", overflow: "hidden", backgroundColor: "var(--vivaan-taupe)", ...style, }; return (
setHovered(true)} onMouseLeave={() => setHovered(false)} onFocus={() => setHovered(true)} onBlur={() => setHovered(false)} tabIndex={0} > {images.map((src, i) => (
))} {caption ? (
{caption}
) : null}
); } // Slow, editorial crossfade between a set of photographs — reusable anywhere // a single PhotoBlock previously stood in for real imagery. Matches the // brand's "luxury pace": long hold, slow fade, no motion on the images. function PhotoSlideshow({ images, ratio, caption, style, holdMs = 4500, fadeMs = 1500 }) { const [active, setActive] = React.useState(0); React.useEffect(() => { if (!images || images.length < 2) return; const id = setInterval(() => { setActive((v) => (v + 1) % images.length); }, holdMs); return () => clearInterval(id); }, [images, holdMs]); const wrap = { position: "relative", aspectRatio: ratio || "4 / 5", overflow: "hidden", backgroundColor: "var(--vivaan-taupe)", ...style, }; const cap = { position: "absolute", left: 20, bottom: 18, right: 20, zIndex: 2, color: "var(--color-text-on-dark)", fontFamily: "var(--font-serif)", fontSize: 11, letterSpacing: "0.18em", textTransform: "uppercase", opacity: 0.9, }; return (
{(images || []).map((src, i) => (
))} {caption ?
{caption}
: null}
); } function SectionLabel({ children, dark, style }) { return ( {children} ); } // parts: [{ text, italic }] function EditorialHeading({ parts, size = 48, dark, style }) { // Fluid type: scales between ~58% and 100% of the given size across the // viewport, so every headline built on this component (home + inner pages) // is responsive without per-callsite breakpoint work. const fluidSize = typeof size === "number" ? `clamp(${Math.round(size * 0.58)}px, 6vw, ${size}px)` : size; return (

{parts.map((p, i) => ( {p.text} ))}

); } function BodyCopy({ children, dark, style }) { return (

{children}

); } /* ---------------------------------------------------------------------- */ /* Navigation + Footer */ /* ---------------------------------------------------------------------- */ const NAV_LINKS = [ { label: "Stay", href: "rooms.html" }, { label: "Dining", href: "dining.html" }, { label: "Wedding & Events", href: "wedding.html" }, { label: "Meetings & Conferences", href: "corporate.html" }, ]; const VIVAAN_LOGO_SRC = (typeof window !== "undefined" && window.__resources && window.__resources.logoImg) ? window.__resources.logoImg : "assets/vivaan-logo-white.png"; function SiteNav() { const { NavigationBar } = window.VivaanDesignSystem_b1c8ba; const [scrolled, setScrolled] = React.useState(false); const [menuOpen, setMenuOpen] = React.useState(false); React.useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 60); onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); const navBg = scrolled ? "var(--vivaan-overlay-dark)" : "var(--color-surface-overlay-nav)"; const logo = ( The Vivaan Hotel & Resort, Karnal ); return (
{/* Desktop / laptop / landscape-tablet nav (>=900px) — the DS NavigationBar as-is */}
The Vivaan Hotel & Resort, Karnal} links={NAV_LINKS} cta={} style={{ backgroundColor: navBg, padding: "12px 40px", gap: "24px", transition: "background-color 0.3s ease", }} />
{/* Mobile / portrait-tablet nav (<900px) — compact bar + slide-down menu */}
{logo}
{menuOpen ? (
{NAV_LINKS.map((l) => ( setMenuOpen(false)}>{l.label} ))}
) : null}
); } function SocialGlyph({ label, href, background, children }) { return ( {children} ); } function InstagramGlyph() { return ( ); } function FacebookGlyph() { return ( ); } function YouTubeGlyph() { return ( ); } function LinkedInGlyph() { return ( ); } function SiteFooter() { const wrap = { position: "relative", backgroundColor: "var(--vivaan-black)", color: "var(--color-text-on-dark)", overflow: "hidden", fontFamily: "var(--font-serif)", }; const inner = { position: "relative", zIndex: 1, padding: "var(--space-2xl) var(--space-xl) var(--space-lg)", display: "flex", flexDirection: "column", gap: "var(--space-xl)", }; const watermark = { position: "absolute", right: "-40px", bottom: "-120px", fontSize: "560px", lineHeight: 1, fontWeight: 400, color: "rgba(255,255,255,0.04)", userSelect: "none", pointerEvents: "none", }; const top = { display: "flex", justifyContent: "space-between", gap: "var(--space-xl)", flexWrap: "wrap" }; const colTitle = { fontSize: 11, letterSpacing: "0.18em", textTransform: "uppercase", color: "rgba(255,255,255,0.5)", marginBottom: 16 }; const linkStyle = { display: "block", fontSize: 14, color: "var(--color-text-on-dark)", textDecoration: "none", opacity: 0.85, marginBottom: 10, }; const rule = { borderTop: "1px solid rgba(255,255,255,0.15)", paddingTop: "var(--space-sm)", display: "flex", justifyContent: "space-between", fontSize: 11, opacity: 0.55, letterSpacing: "0.05em", flexWrap: "wrap", gap: 12, }; return ( ); } /* ---------------------------------------------------------------------- */ /* Page-header (for inner pages — a shorter hero band, not the full 100vh) */ /* ---------------------------------------------------------------------- */ function PageHeader({ eyebrow, parts, body, tone = "ink", images, video, vimeoId }) { return (
{vimeoId ? (