<!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>