<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Video Library</title> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet" /> <style> /* All styles scoped under .vlib so they never leak into the Stonly host page */ .vlib, .vlib * { box-sizing: border-box; } .vlib { --accent: #F26A21; --ink: #141428; --ink-2: #3a3a52; --muted: #6a6a80; --line: #eceef2; font-family: 'Nunito', system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: transparent; color: var(--ink); -webkit-font-smoothing: antialiased; } .vlib button, .vlib select, .vlib input { font-family: inherit; } .vlib h1, .vlib h2, .vlib h3, .vlib p { margin: 0; } /* Layout */ .vlib-main { max-width: 1200px; margin: 0 auto; padding: 8px 4px 40px; } .vlib-intro { margin-bottom: 24px; } .vlib-intro h1 { font-size: 28px; font-weight: 900; line-height: 1.1; letter-spacing: -0.6px; margin-bottom: 8px; text-wrap: balance; } .vlib-intro p { font-size: 14px; line-height: 1.55; color: var(--ink-2); max-width: 640px; text-wrap: pretty; } .vlib-filters { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; padding-bottom: 18px; border-bottom: 1px solid var(--line); } .vlib-cats { display: flex; gap: 6px; flex-wrap: wrap; flex: 1; min-width: 260px; } .vlib-selects { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; } .vlib-count { font-size: 11px; font-weight: 800; letter-spacing: 1.4px; text-transform: uppercase; color: var(--muted); margin-bottom: 14px; } .vlib-grid { display: grid; gap: 22px; grid-template-columns: repeat(3, minmax(0, 1fr)); } .vlib-grid[data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } .vlib-grid[data-cols="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); } /* Pill buttons */ .vlib-pill { padding: 7px 16px; border-radius: 999px; border: 1.5px solid var(--line); background: #fff; color: var(--ink); font-size: 11px; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase; cursor: pointer; transition: all .15s; } .vlib-pill:hover { border-color: var(--accent); color: var(--accent); } .vlib-pill[data-active="true"] { background: var(--accent); border-color: var(--accent); color: #fff; } .vlib-pill[data-active="true"]:hover { color: #fff; } /* Selects */ .vlib-select-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase; color: var(--muted); } .vlib-select-wrap { position: relative; } .vlib-select-wrap select { appearance: none; -webkit-appearance: none; padding: 7px 30px 7px 12px; border-radius: 999px; border: 1.5px solid var(--line); background: #fff; color: var(--ink); font-size: 11px; font-weight: 800; letter-spacing: 0.6px; text-transform: uppercase; cursor: pointer; } .vlib-select-wrap svg { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--muted); } /* Card */ .vlib-card { cursor: pointer; } .vlib-thumb { position: relative; border-radius: 14px; overflow: hidden; aspect-ratio: 16 / 9; box-shadow: 0 4px 12px -4px rgba(20,20,40,0.1); transition: transform .25s cubic-bezier(.2,.8,.2,1), box-shadow .25s; } .vlib-card:hover .vlib-thumb { transform: translateY(-2px); box-shadow: 0 12px 32px -8px rgba(20,20,40,0.22); } .vlib-thumb svg.bg { width: 100%; height: 100%; display: block; } .vlib-duration { position: absolute; bottom: 10px; right: 10px; background: rgba(18,18,28,0.85); color: #fff; font-size: 11px; font-weight: 600; padding: 3px 7px; border-radius: 5px; font-family: ui-monospace, Menlo, monospace; } .vlib-cat { position: absolute; top: 10px; left: 10px; background: rgba(255,255,255,0.95); color: #1a1a2e; font-size: 10px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase; padding: 4px 9px; border-radius: 999px; } .vlib-play-wrap { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: transparent; transition: background .25s; pointer-events: none; } .vlib-card:hover .vlib-play-wrap { background: rgba(18,18,28,0.25); } .vlib-play { width: 52px; height: 52px; border-radius: 50%; background: var(--accent); display: flex; align-items: center; justify-content: center; transform: scale(0.85); opacity: 0; transition: transform .25s cubic-bezier(.2,.8,.2,1), opacity .25s; box-shadow: 0 8px 24px rgba(0,0,0,0.3); } .vlib-card:hover .vlib-play { transform: scale(1); opacity: 1; } .vlib-card-meta { padding-top: 12px; } .vlib-card-meta h3 { font-size: 15px; font-weight: 800; line-height: 1.3; letter-spacing: -0.2px; margin-bottom: 6px; text-wrap: balance; } .vlib-card-meta .sub { font-size: 12px; color: var(--muted); font-weight: 600; } /* Modal */ .vlib-modal-backdrop { position: fixed; inset: 0; z-index: 100; background: rgba(14,14,24,0.78); backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; padding: 32px; animation: vlibFadeIn .22s ease-out; } .vlib-modal-card { width: 100%; max-width: 1180px; max-height: 92vh; background: #fff; border-radius: 20px; overflow: hidden; display: grid; grid-template-columns: minmax(0, 1fr) 340px; box-shadow: 0 40px 120px -20px rgba(0,0,0,0.5); animation: vlibSlideUp .28s cubic-bezier(.2,.8,.2,1); } .vlib-modal-main { display: flex; flex-direction: column; min-height: 0; min-width: 0; position: relative; } .vlib-modal-video { position: relative; background: #000; aspect-ratio: 16 / 9; } .vlib-modal-video wistia-player { width: 100%; height: 100%; display: block; } .vlib-close { position: absolute; top: 14px; right: 14px; width: 38px; height: 38px; border-radius: 50%; background: rgba(18,18,28,0.7); color: #fff; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 5; } .vlib-modal-info { padding: 24px 28px 28px; overflow-y: auto; flex: 1; } .vlib-modal-info .eyebrow { display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: 1.2px; text-transform: uppercase; color: var(--accent); margin-bottom: 10px; } .vlib-modal-info h2 { font-size: 24px; font-weight: 800; line-height: 1.2; letter-spacing: -0.4px; margin-bottom: 14px; text-wrap: balance; } .vlib-modal-info .byline { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; flex-wrap: wrap; } .vlib-modal-info .presenter { display: flex; align-items: center; gap: 10px; } .vlib-avatar { width: 34px; height: 34px; border-radius: 50%; color: #fff; font-weight: 800; font-size: 13px; display: flex; align-items: center; justify-content: center; } .vlib-modal-info .presenter-name { font-size: 13px; font-weight: 700; } .vlib-modal-info .presenter-role { font-size: 12px; color: var(--muted); } .vlib-modal-info .meta-dot { color: var(--muted); font-size: 13px; } .vlib-modal-info .meta { font-size: 13px; color: var(--muted); } .vlib-modal-info p { font-size: 15px; line-height: 1.6; color: var(--ink-2); text-wrap: pretty; } .vlib-upnext { border-left: 1px solid var(--line); background: #fafafb; padding: 22px; overflow-y: auto; } .vlib-upnext .label { font-size: 12px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); margin-bottom: 14px; } .vlib-upnext-list { display: flex; flex-direction: column; gap: 10px; } .vlib-upnext-item { display: grid; grid-template-columns: 110px 1fr; gap: 12px; padding: 8px; border-radius: 12px; background: transparent; border: none; cursor: pointer; text-align: left; transition: background .15s; width: 100%; } .vlib-upnext-item:hover { background: #fff; } .vlib-upnext-thumb { position: relative; aspect-ratio: 16/9; border-radius: 8px; overflow: hidden; } .vlib-upnext-thumb .mini-dur { position: absolute; bottom: 4px; right: 4px; background: rgba(18,18,28,0.85); color: #fff; font-size: 10px; font-weight: 600; padding: 2px 5px; border-radius: 4px; font-family: ui-monospace, Menlo, monospace; } .vlib-upnext-item .cat { font-size: 10px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase; color: var(--accent); margin-bottom: 4px; } .vlib-upnext-item .title { font-size: 13px; font-weight: 700; line-height: 1.3; color: var(--ink); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .vlib-upnext-item .views { font-size: 11px; color: var(--muted); margin-top: 4px; } /* Tweaks */ .vlib-tweaks { position: fixed; bottom: 20px; right: 20px; z-index: 200; background: #fff; border-radius: 14px; box-shadow: 0 20px 60px -10px rgba(20,20,40,0.25), 0 0 0 1px rgba(20,20,40,0.06); padding: 18px; width: 268px; display: none; } .vlib-tweaks[data-visible="true"] { display: block; } .vlib-tweaks .header { font-size: 11px; font-weight: 800; letter-spacing: 1.4px; text-transform: uppercase; color: var(--muted); margin-bottom: 14px; } .vlib-tweaks .group { margin-bottom: 16px; } .vlib-tweaks .group:last-child { margin-bottom: 0; } .vlib-tweaks .group .label { font-size: 12px; font-weight: 700; margin-bottom: 8px; } .vlib-accents { display: flex; gap: 8px; } .vlib-accent-btn { width: 28px; height: 28px; border-radius: 50%; border: 3px solid transparent; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08); cursor: pointer; padding: 0; } .vlib-accent-btn[data-active="true"] { border-color: #fff; box-shadow: 0 0 0 2px currentColor; } .vlib-seg { display: flex; gap: 6px; background: #f4f4f7; padding: 4px; border-radius: 8px; } .vlib-seg button { flex: 1; padding: 8px 10px; border-radius: 6px; background: transparent; color: var(--muted); border: none; cursor: pointer; font-size: 12px; font-weight: 700; } .vlib-seg button[data-active="true"] { background: #fff; color: var(--ink); box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .vlib-empty { padding: 60px 20px; text-align: center; background: #fff; border-radius: 14px; border: 1px dashed var(--line); } .vlib-empty .h { font-size: 16px; font-weight: 800; margin-bottom: 6px; } .vlib-empty .s { font-size: 13px; color: var(--muted); } @keyframes vlibFadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes vlibSlideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @media (max-width: 720px) { .vlib-grid { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } .vlib-modal-card { grid-template-columns: 1fr; max-height: 100vh; border-radius: 0; } .vlib-upnext { display: none; } .vlib-modal-backdrop { padding: 0; } } </style> </head> <body style="margin:0"> <div id="vlib-root" class="vlib"></div> <script> (function () { // ───────────────────────────────────────────────────────── // DATA const WISTIA_IDS = ["3e7mmrzhpp", "tdzkzt6xws", "yk9tl7kbtg", "p9wuz1col5"]; const VIDEOS = [ { id: "v1", mediaId: WISTIA_IDS[0], title: "What is Commerce Media? A 60-second primer", description: "An introduction to Commerce Media and how it connects retailers, brands, and shoppers across the entire purchase journey.", category: "Fundamentals", duration: 64, date: "2026-04-18", views: 12400, presenter: "Maya Okafor", role: "Head of Commerce Strategy", hue: 22 }, { id: "v2", mediaId: WISTIA_IDS[1], title: "Retail Media Networks, explained", description: "How RMNs work, why every major retailer is building one, and what it means for brand budgets in 2026.", category: "Retail Media", duration: 72, date: "2026-04-11", views: 8900, presenter: "Jonas Brandt", role: "Director, Retail Partnerships", hue: 210 }, { id: "v3", mediaId: WISTIA_IDS[2], title: "Onsite vs offsite: where your ad dollars work hardest", description: "A quick comparison of onsite and offsite commerce media placements, with real performance benchmarks across categories.", category: "Performance", duration: 58, date: "2026-04-04", views: 6200, presenter: "Priya Shankar", role: "Senior Measurement Lead", hue: 150 }, { id: "v4", mediaId: WISTIA_IDS[3], title: "Closed-loop measurement in 3 minutes", description: "Understanding how commerce media closes the gap between ad spend and sales — and why that changes every conversation with your CFO.", category: "Measurement", duration: 182, date: "2026-03-28", views: 5100, presenter: "Tomás Velázquez", role: "VP Analytics", hue: 280 }, { id: "v5", mediaId: WISTIA_IDS[0], title: "Building your product catalog, the right way", description: "A short walkthrough on structuring product feeds for maximum ad performance. Attributes, images, and the mistakes most brands make.", category: "Getting Started", duration: 95, date: "2026-03-21", views: 4400, presenter: "Anika Chen", role: "Product Manager, Catalogs", hue: 45 }, { id: "v6", mediaId: WISTIA_IDS[1], title: "The death of third-party cookies — what commerce teams do next", description: "First-party data, commerce signals, and how to build an identity strategy that doesn't depend on cookies.", category: "Industry", duration: 88, date: "2026-03-14", views: 9800, presenter: "Leo Martens", role: "Head of Identity", hue: 345 }, { id: "v7", mediaId: WISTIA_IDS[2], title: "Incrementality testing, without the headache", description: "How to design a clean incrementality test for your commerce media spend in under an hour. Sample sizes, holdouts, and what to report.", category: "Measurement", duration: 121, date: "2026-03-07", views: 3600, presenter: "Fatima Noor", role: "Principal Data Scientist", hue: 190 }, { id: "v8", mediaId: WISTIA_IDS[3], title: "From browsers to buyers: the commerce funnel", description: "Mapping commerce media tactics to each funnel stage — awareness, consideration, conversion, and retention — with real examples.", category: "Performance", duration: 76, date: "2026-02-28", views: 7200, presenter: "Maya Okafor", role: "Head of Commerce Strategy", hue: 15 }, { id: "v9", mediaId: WISTIA_IDS[0], title: "Creative that converts in commerce placements", description: "What makes a product ad actually sell. Hooks, formats, and the 3-second rule — illustrated with side-by-sides.", category: "Creative", duration: 102, date: "2026-02-21", views: 5800, presenter: "Rio Takahashi", role: "Creative Director", hue: 320 }, ]; const CATEGORIES = ["All", "Fundamentals", "Retail Media", "Performance", "Measurement", "Getting Started", "Industry", "Creative"]; const DURATIONS = [ { id: "any", label: "Any length" }, { id: "short", label: "Under 1 min", test: (s) => s < 60 }, { id: "mid", label: "1–2 min", test: (s) => s >= 60 && s <= 120 }, { id: "long", label: "Over 2 min", test: (s) => s > 120 }, ]; const SORTS = [ { id: "newest", label: "Newest" }, { id: "popular", label: "Most popular" }, { id: "shortest", label: "Shortest first" }, ]; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "#F26A21", "cols": 3, "thumbStyle": "photo" }/*EDITMODE-END*/; let state = { ...TWEAK_DEFAULTS, cat: "All", dur: "any", sort: "newest", open: null, tweaksVisible: false, }; // Wistia: lazy-load. We only inject the main player.js + per-video embed // script the first time a modal opens. Keeps initial page load ~5KB total. const wistiaLoaded = new Set(); function loadWistia(mediaId) { if (!window.__vlibPlayerLoaded) { window.__vlibPlayerLoaded = true; const s = document.createElement("script"); s.src = "https://fast.wistia.com/player.js"; s.async = true; document.head.appendChild(s); } if (!wistiaLoaded.has(mediaId)) { wistiaLoaded.add(mediaId); const s = document.createElement("script"); s.src = `https://fast.wistia.com/embed/${mediaId}.js`; s.async = true; s.type = "module"; document.head.appendChild(s); } } // ───────────────────────────────────────────────────────── // HELPERS const h = (tag, props, ...kids) => { const el = document.createElement(tag); if (props) { for (const k in props) { if (k === "class") el.className = props[k]; else if (k === "style") el.setAttribute("style", props[k]); else if (k === "html") el.innerHTML = props[k]; else if (k.startsWith("on")) el.addEventListener(k.slice(2).toLowerCase(), props[k]); else if (k.startsWith("data-") || k === "role" || k === "aria-label" || k === "title" || k === "type" || k === "value" || k === "placeholder") el.setAttribute(k, props[k]); else el[k] = props[k]; } } for (const k of kids.flat()) { if (k == null || k === false) continue; el.appendChild(k.nodeType ? k : document.createTextNode(String(k))); } return el; }; const fmtDuration = (s) => `${Math.floor(s/60)}:${(s%60).toString().padStart(2,"0")}`; const fmtViews = (n) => n >= 1000 ? (n/1000).toFixed(1).replace(/\.0$/,"") + "K views" : n + " views"; const fmtDate = (iso) => { const d = new Date(iso), now = new Date("2026-04-24"); const days = Math.round((now - d) / 86400000); if (days < 7) return `${days}d ago`; if (days < 30) return `${Math.round(days/7)}w ago`; return `${Math.round(days/30)}mo ago`; }; // ───────────────────────────────────────────────────────── // Abstract editorial motif per card. Built as an SVG string (fastest path). function thumbArtSVG(video, idx) { const hue = video.hue; const bg = `oklch(0.62 0.18 ${hue})`; const fg = `oklch(0.92 0.08 ${hue})`; const dark = `oklch(0.35 0.14 ${hue})`; const accent = `oklch(0.78 0.16 ${(hue+40)%360})`; const motif = idx % 6; const inner = [ `<circle cx="110" cy="112" r="78" fill="${fg}" opacity="0.35"/><circle cx="110" cy="112" r="54" fill="none" stroke="${fg}" stroke-width="3" opacity="0.9"/><rect x="210" y="60" width="140" height="10" fill="${fg}" opacity="0.6"/><rect x="210" y="82" width="100" height="10" fill="${fg}" opacity="0.4"/><rect x="210" y="104" width="120" height="10" fill="${fg}" opacity="0.25"/>`, `<path d="M0,180 Q100,120 200,150 T400,130 L400,225 L0,225 Z" fill="${dark}" opacity="0.5"/><path d="M0,200 Q120,150 220,180 T400,170 L400,225 L0,225 Z" fill="${fg}" opacity="0.35"/><circle cx="320" cy="60" r="28" fill="${accent}"/>`, `<rect x="40" y="40" width="100" height="145" fill="${fg}" opacity="0.85"/><rect x="160" y="70" width="100" height="115" fill="${accent}" opacity="0.9"/><rect x="280" y="55" width="80" height="130" fill="${dark}" opacity="0.7"/><line x1="40" y1="185" x2="360" y2="185" stroke="${fg}" stroke-width="2" opacity="0.6"/>`, [0,1,2,3,4,5,6,7].map(i => `<rect x="${30+i*45}" y="${200-(i%3)*30-40}" width="28" height="${40+(i%3)*30}" fill="${fg}" opacity="${0.4+(i%3)*0.2}"/>`).join(""), `<circle cx="200" cy="112" r="95" fill="none" stroke="${fg}" stroke-width="3" opacity="0.5"/><circle cx="200" cy="112" r="65" fill="none" stroke="${fg}" stroke-width="3" opacity="0.7"/><circle cx="200" cy="112" r="35" fill="${fg}" opacity="0.9"/><circle cx="200" cy="112" r="12" fill="${dark}"/>`, `<polygon points="50,180 130,60 210,180" fill="${fg}" opacity="0.7"/><polygon points="180,180 260,90 340,180" fill="${accent}" opacity="0.85"/><rect x="0" y="180" width="400" height="4" fill="${dark}" opacity="0.6"/>`, ][motif]; return `<svg class="bg" viewBox="0 0 400 225" preserveAspectRatio="xMidYMid slice"><rect width="400" height="225" fill="${bg}"/>${inner}</svg>`; } function cardEl(video, idx, onOpen, mini) { const thumbInner = state.thumbStyle === "typographic" && !mini ? `<div style="width:100%;height:100%;background:oklch(0.62 0.18 ${video.hue});display:flex;flex-direction:column;justify-content:flex-end;padding:20px;color:#fff;"><div style="font-size:11px;font-weight:700;letter-spacing:1.4px;text-transform:uppercase;opacity:0.85;margin-bottom:8px;">${video.category}</div><div style="font-size:20px;font-weight:800;line-height:1.15;letter-spacing:-0.4px;">${video.title}</div></div>` : thumbArtSVG(video, idx); if (mini) { const btn = h("button", { class: "vlib-upnext-item", onClick: () => onOpen(video) }, h("div", { class: "vlib-upnext-thumb", html: thumbInner + `<div class="mini-dur">${fmtDuration(video.duration)}</div>` }), h("div", {}, h("div", { class: "cat" }, video.category), h("div", { class: "title" }, video.title), h("div", { class: "views" }, fmtViews(video.views)), ), ); return btn; } return h("div", { class: "vlib-card", onClick: () => onOpen(video) }, h("div", { class: "vlib-thumb", html: thumbInner + ` <div class="vlib-duration">${fmtDuration(video.duration)}</div> <div class="vlib-cat">${video.category}</div> <div class="vlib-play-wrap"><div class="vlib-play"><svg width="18" height="18" viewBox="0 0 20 20"><path d="M6 4.5 L16 10 L6 15.5 Z" fill="#fff"/></svg></div></div> `, }), h("div", { class: "vlib-card-meta" }, h("h3", {}, video.title), h("div", { class: "sub" }, `${video.presenter} · ${fmtViews(video.views)} · ${fmtDate(video.date)}`), ), ); } // ───────────────────────────────────────────────────────── function openModal(video) { loadWistia(video.mediaId); state.open = video; renderModal(); } function closeModal() { state.open = null; document.body.style.overflow = ""; const existing = document.getElementById("vlib-modal"); if (existing) existing.remove(); } function renderModal() { const existing = document.getElementById("vlib-modal"); if (existing) existing.remove(); if (!state.open) return; document.body.style.overflow = "hidden"; const v = state.open; const upNext = VIDEOS.filter(x => x.id !== v.id).slice(0, 5); const initials = v.presenter.split(" ").map(p => p[0]).join("").slice(0, 2); const main = h("div", { class: "vlib-modal-main" }, h("div", { class: "vlib-modal-video", html: `<wistia-player media-id="${v.mediaId}" aspect="1.7777777777777777" autoplay="true"></wistia-player>`, }), h("button", { class: "vlib-close", "aria-label": "Close", onClick: closeModal, html: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 3 L13 13 M13 3 L3 13" stroke="white" stroke-width="2" stroke-linecap="round"/></svg>', }), h("div", { class: "vlib-modal-info" }, h("div", { class: "eyebrow" }, `${v.category} · ${fmtDuration(v.duration)}`), h("h2", {}, v.title), h("div", { class: "byline" }, h("div", { class: "presenter" }, h("div", { class: "vlib-avatar", style: `background: oklch(0.75 0.12 ${v.hue});` }, initials), h("div", {}, h("div", { class: "presenter-name" }, v.presenter), h("div", { class: "presenter-role" }, v.role), ), ), h("div", { class: "meta-dot" }, "·"), h("div", { class: "meta" }, `${fmtViews(v.views)} · ${fmtDate(v.date)}`), ), h("p", {}, v.description), ), ); const aside = h("div", { class: "vlib-upnext" }, h("div", { class: "label" }, "Up next"), h("div", { class: "vlib-upnext-list" }, ...upNext.map((u, i) => cardEl(u, i, openModal, true)), ), ); const card = h("div", { class: "vlib-modal-card" }, main, aside); const backdrop = h("div", { id: "vlib-modal", class: "vlib-modal-backdrop", onClick: (e) => { if (e.target === e.currentTarget) closeModal(); }, }, card); document.body.appendChild(backdrop); // Esc to close const onKey = (e) => { if (e.key === "Escape") { closeModal(); window.removeEventListener("keydown", onKey); } }; window.addEventListener("keydown", onKey); } // ───────────────────────────────────────────────────────── function getFiltered() { let list = VIDEOS.slice(); if (state.cat !== "All") list = list.filter(v => v.category === state.cat); const d = DURATIONS.find(d => d.id === state.dur); if (d && d.test) list = list.filter(v => d.test(v.duration)); if (state.sort === "newest") list.sort((a,b) => b.date.localeCompare(a.date)); if (state.sort === "popular") list.sort((a,b) => b.views - a.views); if (state.sort === "shortest") list.sort((a,b) => a.duration - b.duration); return list; } function renderApp() { const root = document.getElementById("vlib-root"); root.style.setProperty("--accent", state.accent); root.innerHTML = ""; const catsEl = h("div", { class: "vlib-cats" }, ...CATEGORIES.map(c => h("button", { class: "vlib-pill", "data-active": String(state.cat === c), onClick: () => { state.cat = c; renderApp(); }, }, c)), ); const mkSelect = (label, value, options, onChange) => h("label", { class: "vlib-select-label" }, label, h("div", { class: "vlib-select-wrap", html: `<select>${options.map(o => `<option value="${o.id}"${o.id===value?' selected':''}>${o.label}</option>`).join("")}</select> <svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 4 L5 7 L8 4" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`, }), ); const durSel = mkSelect("Length", state.dur, DURATIONS); durSel.querySelector("select").addEventListener("change", (e) => { state.dur = e.target.value; renderApp(); }); const sortSel = mkSelect("Sort", state.sort, SORTS); sortSel.querySelector("select").addEventListener("change", (e) => { state.sort = e.target.value; renderApp(); }); const filtersEl = h("div", { class: "vlib-filters" }, catsEl, h("div", { class: "vlib-selects" }, durSel, sortSel)); const filtered = getFiltered(); const countEl = h("div", { class: "vlib-count" }, `${filtered.length} ${filtered.length === 1 ? "video" : "videos"}`); let gridEl; if (filtered.length === 0) { gridEl = h("div", { class: "vlib-empty" }, h("div", { class: "h" }, "No videos match those filters"), h("div", { class: "s" }, "Try clearing a category or length."), ); } else { gridEl = h("div", { class: "vlib-grid", "data-cols": String(state.cols) }, ...filtered.map((v, i) => cardEl(v, i, openModal, false)), ); } const main = h("main", { class: "vlib-main" }, h("div", { class: "vlib-intro" }, h("h1", {}, "Commerce Media video library"), h("p", {}, "Short explainers, product walkthroughs, and deep-dives on commerce media. Click any thumbnail to watch."), ), filtersEl, countEl, gridEl, ); root.appendChild(main); root.appendChild(buildTweaksPanel()); } // ───────────────────────────────────────────────────────── function buildTweaksPanel() { const accents = [ { value: "#F26A21", label: "Orange" }, { value: "#5C4DFF", label: "Indigo" }, { value: "#0E9F8A", label: "Teal" }, { value: "#E0305B", label: "Crimson" }, { value: "#1E293B", label: "Slate" }, ]; const update = (patch) => { Object.assign(state, patch); window.parent.postMessage({ type: "__edit_mode_set_keys", edits: patch }, "*"); renderApp(); }; const accentRow = h("div", { class: "vlib-accents" }, ...accents.map(a => { const btn = h("button", { class: "vlib-accent-btn", "data-active": String(state.accent === a.value), title: a.label, style: `background:${a.value};color:${a.value};`, onClick: () => update({ accent: a.value }), }); return btn; }), ); const densityRow = h("div", { class: "vlib-seg" }, ...[2,3,4].map(n => h("button", { "data-active": String(state.cols === n), onClick: () => update({ cols: n }), }, `${n} cols`)), ); const styleRow = h("div", { class: "vlib-seg" }, ...[{id:"photo",label:"Editorial"},{id:"typographic",label:"Typographic"}].map(o => h("button", { "data-active": String(state.thumbStyle === o.id), onClick: () => update({ thumbStyle: o.id }), }, o.label)), ); return h("div", { class: "vlib-tweaks", "data-visible": String(state.tweaksVisible) }, h("div", { class: "header" }, "Tweaks"), h("div", { class: "group" }, h("div", { class: "label" }, "Accent color"), accentRow), h("div", { class: "group" }, h("div", { class: "label" }, "Grid density"), densityRow), h("div", { class: "group" }, h("div", { class: "label" }, "Thumbnail style"), styleRow), ); } // Tweaks pane handshake with host toolbar window.addEventListener("message", (e) => { const d = e.data || {}; if (d.type === "__activate_edit_mode") { state.tweaksVisible = true; renderApp(); } if (d.type === "__deactivate_edit_mode") { state.tweaksVisible = false; renderApp(); } }); window.parent.postMessage({ type: "__edit_mode_available" }, "*"); renderApp(); })(); </script> </body> </html>