From 8e0081ce2c6675054037d880edcd5f4ce8981114 Mon Sep 17 00:00:00 2001 From: Adrian Llopart Date: Thu, 18 Jun 2026 13:55:57 +0200 Subject: [PATCH 01/24] feat: add autoscroll video showcase, replace "Run it three ways" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new "03 — See it run" section directly under the architecture diagram: a tabbed (Benchmarks / Simulation / Deployment) autoscroll of square, rounded clips. Each clip autoplays muted on loop; hovering any clip pauses the row and reveals a fullscreen affordance; clicking opens a full-resolution modal (Escape / backdrop / close button dismiss it, restoring autoscroll). Clips are auto-discovered at build time via import.meta.glob from src/videos// — drop a file named __.mp4 and it appears, with the benchmark/rskill chips and the SUCCESS/FAIL status pill parsed from the filename. Status dots use the existing --live / --red tokens; modal is portaled to body so it layers above the nav. Respects prefers-reduced-motion (static, scrollable; no autoplay). Remove the now-deprecated "Run it three ways" block from the architecture section (its Benchmark/Simulate/Deploy tabs are superseded by the showcase) and its orphaned CSS. Renumber section eyebrows to stay sequential and reorder the nav link to match scroll order. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01PxVX8QDC2gmULaJxXN3tXu --- src/App.jsx | 2 + src/components/ArchitectureDiagram.css | 65 ----- src/components/ArchitectureDiagram.jsx | 55 +--- src/components/Capabilities.jsx | 2 +- src/components/Contact.jsx | 2 +- src/components/Nav.jsx | 1 + src/components/RSkills.jsx | 2 +- src/components/Solve.jsx | 2 +- src/components/VideoShowcase.css | 351 +++++++++++++++++++++++++ src/components/VideoShowcase.jsx | 244 +++++++++++++++++ src/videos/README.md | 43 +++ src/videos/benchmarks/.gitkeep | 0 src/videos/deployment/.gitkeep | 0 src/videos/simulation/.gitkeep | 0 14 files changed, 646 insertions(+), 123 deletions(-) create mode 100644 src/components/VideoShowcase.css create mode 100644 src/components/VideoShowcase.jsx create mode 100644 src/videos/README.md create mode 100644 src/videos/benchmarks/.gitkeep create mode 100644 src/videos/deployment/.gitkeep create mode 100644 src/videos/simulation/.gitkeep diff --git a/src/App.jsx b/src/App.jsx index 503ec13..8d15b4c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,6 +6,7 @@ import ArchitectureDiagram from "./components/ArchitectureDiagram.jsx"; import Solve from "./components/Solve.jsx"; import Capabilities from "./components/Capabilities.jsx"; import RSkills from "./components/RSkills.jsx"; +import VideoShowcase from "./components/VideoShowcase.jsx"; import Contact from "./components/Contact.jsx"; import Footer from "./components/Footer.jsx"; import Privacy from "./components/Privacy.jsx"; @@ -29,6 +30,7 @@ export default function App() { + diff --git a/src/components/ArchitectureDiagram.css b/src/components/ArchitectureDiagram.css index 1481bbf..eeea10d 100644 --- a/src/components/ArchitectureDiagram.css +++ b/src/components/ArchitectureDiagram.css @@ -118,71 +118,6 @@ border-style: dashed; } -/* ---- run modes (segmented panel) ---- */ -.modes { - margin-top: 34px; -} -.mode-head { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 12px; - margin-bottom: 14px; -} -.mode-label { - font-family: var(--mono); - font-size: 12px; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--muted); -} -.mode-tabs { - display: inline-flex; - gap: 5px; - padding: 4px; - background: var(--bg-2); - border: 1px solid var(--hair-2); - border-radius: 11px; -} -.mode-tab { - font-family: var(--mono); - font-size: 12px; - color: var(--text-2); - background: transparent; - border: none; - border-radius: 8px; - padding: 9px 16px; - min-height: 40px; - cursor: pointer; - transition: color 0.2s, background 0.2s; -} -.mode-tab:hover { - color: var(--text); -} -.mode-tab.active { - color: #0b0c10; - background: #fff; - font-weight: 500; -} -.mode-panel { - background: var(--surface); - border: 1px solid var(--hair); - border-radius: 14px; - padding: clamp(24px, 3vw, 32px); - min-height: 150px; -} -.mode-panel h3 { - font-size: 20px; - margin-bottom: 10px; -} -.mode-panel p { - color: var(--text-2); - font-size: 15px; - line-height: 1.65; - max-width: 70ch; -} - @media (prefers-reduced-motion: reduce) { .edge-flow { display: none; diff --git a/src/components/ArchitectureDiagram.jsx b/src/components/ArchitectureDiagram.jsx index 605f641..81401dd 100644 --- a/src/components/ArchitectureDiagram.jsx +++ b/src/components/ArchitectureDiagram.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { motion, AnimatePresence, useReducedMotion } from "framer-motion"; +import { motion, useReducedMotion } from "framer-motion"; import { NODES, EDGES } from "../data/layers.js"; import { useReveal } from "../hooks/useReveal.js"; import "./ArchitectureDiagram.css"; @@ -60,30 +60,10 @@ function edgePath(e) { return `M ${ax} ${ay} C ${mid} ${ay} ${mid} ${by} ${bx} ${by}`; } -const MODES = [ - { - tag: "Benchmark", - title: "Reproduce the score", - body: "12 reproducible eval configs — LIBERO, MetaWorld, ManiSkill3, SimplerEnv, RoboCasa, gym-aloha, gym-pusht. Every rSkill ships its score.", - }, - { - tag: "Simulate", - title: "Roll out in sim", - body: "Run any rSkill headless in its sim backend — validate a policy before it touches hardware.", - }, - { - tag: "Deploy", - title: "Run the harness", - body: "One typed manifest drives sim and real — perception, reasoning, rSkills and safety, end to end.", - }, -]; - export default function ArchitectureDiagram() { const reveal = useReveal(); const reduce = useReducedMotion(); const [hovered, setHovered] = useState(null); - const [activeMode, setActiveMode] = useState(MODES[0].tag); - const curMode = MODES.find((m) => m.tag === activeMode); const isEdgeHot = (e) => hovered && (e.from === hovered || e.to === hovered); const pct = (v, total) => `${(v / total) * 100}%`; @@ -151,39 +131,6 @@ export default function ArchitectureDiagram() { ))} - - -
- Run it three ways -
- {MODES.map((m) => ( - - ))} -
-
-
- - -

{curMode.title}

-

{curMode.body}

-
-
-
-
); } diff --git a/src/components/Capabilities.jsx b/src/components/Capabilities.jsx index bb7fb1f..adcd80f 100644 --- a/src/components/Capabilities.jsx +++ b/src/components/Capabilities.jsx @@ -55,7 +55,7 @@ export default function Capabilities() { return (
-
04 — What's inside
+
05 — What's inside

A full stack, composed — not reinvented.

diff --git a/src/components/Contact.jsx b/src/components/Contact.jsx index 379b145..65b7d7a 100644 --- a/src/components/Contact.jsx +++ b/src/components/Contact.jsx @@ -90,7 +90,7 @@ export default function Contact() {
-
06 — Get in touch
+
07 — Get in touch

Let's talk.

diff --git a/src/components/Nav.jsx b/src/components/Nav.jsx index ca5084c..d1b69b1 100644 --- a/src/components/Nav.jsx +++ b/src/components/Nav.jsx @@ -5,6 +5,7 @@ import "./Nav.css"; const LINKS = [ { href: "/#install", label: "Install" }, { href: "/#architecture", label: "Architecture" }, + { href: "/#showcase", label: "Showcase" }, { href: "/#solve", label: "Why" }, { href: "/#features", label: "Capabilities" }, { href: "/#rskills", label: "rSkills" }, diff --git a/src/components/RSkills.jsx b/src/components/RSkills.jsx index 08325e6..b74f8ff 100644 --- a/src/components/RSkills.jsx +++ b/src/components/RSkills.jsx @@ -131,7 +131,7 @@ export default function RSkills() { return (
-
05 — rSkills
+
06 — rSkills

rSkills you can publish, score and share.

diff --git a/src/components/Solve.jsx b/src/components/Solve.jsx index 2ee35ff..7066ee2 100644 --- a/src/components/Solve.jsx +++ b/src/components/Solve.jsx @@ -39,7 +39,7 @@ export default function Solve() { return (
-
03 — The problem & our answer
+
04 — The problem & our answer

Robots, models and sensors don't speak the same language. OpenRAL is the contract that makes them. diff --git a/src/components/VideoShowcase.css b/src/components/VideoShowcase.css new file mode 100644 index 0000000..e9492e1 --- /dev/null +++ b/src/components/VideoShowcase.css @@ -0,0 +1,351 @@ +/* ============================================================ + VideoShowcase — tabbed autoscroll of scored run clips + ============================================================ */ + +/* ---- category tabs (same language as .kind-tab) ---- */ +.show-tabs { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 26px; +} +.show-tab { + display: inline-flex; + align-items: center; + gap: 9px; + font-family: var(--mono); + font-size: 13px; + color: var(--text-2); + background: var(--bg-2); + border: 1px solid var(--hair-2); + border-radius: 9px; + padding: 11px 17px; + min-height: 44px; + cursor: pointer; + transition: color 0.2s, border-color 0.2s, background 0.2s; +} +.show-tab:hover { + color: var(--text); + border-color: var(--muted); +} +.show-tab.active { + color: #14100c; + background: var(--accent); + border-color: var(--accent); + font-weight: 500; +} +.show-tab-count { + font-size: 11px; + font-variant-numeric: tabular-nums; + color: var(--muted); + background: var(--accent-soft); + border-radius: 999px; + padding: 1px 7px; + min-width: 20px; + text-align: center; +} +.show-tab.active .show-tab-count { + color: rgba(20, 16, 12, 0.7); + background: rgba(20, 16, 12, 0.12); +} + +/* ---- marquee ---- */ +.show-marquee { + position: relative; + overflow: hidden; + /* fade the two edges into the canvas */ + -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent); + mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent); +} +.show-track { + display: flex; + gap: 16px; + width: max-content; + list-style: none; + padding: 6px 0; + animation: show-scroll var(--marquee-dur, 40s) linear infinite; +} +.show-marquee[data-reverse] .show-track { + animation-direction: reverse; +} +.show-track.is-paused, +.show-marquee:hover .show-track, +.show-marquee:focus-within .show-track { + animation-play-state: paused; +} +@keyframes show-scroll { + from { + transform: translate3d(0, 0, 0); + } + to { + transform: translate3d(-50%, 0, 0); + } +} +.show-item { + flex: 0 0 auto; +} + +/* reduced-motion / no-JS-animation: a plain horizontal scroll region */ +.show-marquee.is-static { + overflow-x: auto; + scroll-snap-type: x mandatory; +} +.show-marquee.is-static .show-track { + animation: none; +} +.show-marquee.is-static .show-item { + scroll-snap-align: start; +} + +/* ---- a clip card (square, rounded, with overlays) ---- */ +.show-card { + position: relative; + display: block; + width: clamp(220px, 24vw, 300px); + aspect-ratio: 1 / 1; + padding: 0; + border: 1px solid var(--hair-2); + border-radius: 16px; + overflow: hidden; + background: var(--bg-2); + cursor: pointer; + transition: transform 0.3s var(--ease), border-color 0.3s var(--ease), box-shadow 0.3s var(--ease); +} +.show-card:hover, +.show-card:focus-visible { + transform: translateY(-3px); + border-color: var(--accent-line); + box-shadow: 0 18px 50px rgba(0, 0, 0, 0.42); +} +.show-video { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +/* legibility wash behind the overlays — top + center, fades out to clean video */ +.show-grad { + position: absolute; + inset: 0; + pointer-events: none; + background: linear-gradient(180deg, rgba(14, 15, 19, 0.62) 0%, rgba(14, 15, 19, 0) 34%), + radial-gradient(120% 70% at 50% 50%, rgba(14, 15, 19, 0.34) 0%, rgba(14, 15, 19, 0) 60%); +} + +/* corner metadata chips */ +.show-chip { + position: absolute; + top: 10px; + max-width: calc(50% - 14px); + font-family: var(--mono); + font-size: 11px; + line-height: 1; + letter-spacing: 0.02em; + color: var(--text); + background: rgba(14, 15, 19, 0.55); + border: 1px solid var(--accent-line); + border-radius: 6px; + padding: 5px 8px; + backdrop-filter: blur(6px); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.show-chip-tl { + left: 10px; + color: #fff; +} +.show-chip-tr { + right: 10px; + color: var(--text-2); +} + +/* center SUCCESS / FAIL pill */ +.show-status { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: inline-flex; + align-items: center; + gap: 8px; + font-family: var(--mono); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.16em; + color: #fff; + background: rgba(14, 15, 19, 0.62); + border: 1px solid var(--accent-line); + border-radius: 999px; + padding: 7px 14px; + backdrop-filter: blur(6px); +} +.show-status.is-inline { + position: static; + transform: none; + font-size: 11px; + padding: 4px 10px; +} +.show-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex: 0 0 auto; + animation: show-blink 1.6s ease-in-out infinite; +} +.show-status-success { + border-color: rgba(79, 181, 139, 0.5); +} +.show-status-success .show-dot { + background: var(--live); + box-shadow: 0 0 10px rgba(79, 181, 139, 0.7); +} +.show-status-fail { + border-color: rgba(217, 138, 142, 0.5); +} +.show-status-fail .show-dot { + background: var(--red); + box-shadow: 0 0 10px rgba(217, 138, 142, 0.6); +} +@keyframes show-blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.25; + } +} + +/* expand affordance — only on hover/focus of the card */ +.show-expand { + position: absolute; + right: 10px; + bottom: 10px; + width: 34px; + height: 34px; + display: grid; + place-items: center; + color: #fff; + background: rgba(14, 15, 19, 0.6); + border: 1px solid var(--accent-line); + border-radius: 9px; + backdrop-filter: blur(6px); + opacity: 0; + transform: scale(0.92); + transition: opacity 0.22s var(--ease), transform 0.22s var(--ease); + pointer-events: none; +} +.show-expand svg { + width: 18px; + height: 18px; +} +.show-card:hover .show-expand, +.show-card:focus-visible .show-expand { + opacity: 1; + transform: scale(1); +} + +/* ---- empty state ---- */ +.show-empty { + border: 1px dashed var(--hair-2); + border-radius: 14px; + padding: 40px 28px; + text-align: center; + color: var(--text-2); + font-size: 14.5px; + background: var(--bg-2); +} +.show-empty code { + font-size: 0.85em; +} + +/* ---- modal ---- */ +.show-overlay { + position: fixed; + inset: 0; + z-index: 1000; + display: grid; + place-items: center; + padding: clamp(16px, 4vw, 48px); + background: rgba(8, 9, 12, 0.74); + backdrop-filter: blur(8px); +} +.show-dialog { + width: min(1100px, 100%); + max-height: 90vh; + display: flex; + flex-direction: column; + background: var(--surface); + border: 1px solid var(--hair-2); + border-radius: 16px; + overflow: hidden; + box-shadow: 0 40px 120px rgba(0, 0, 0, 0.6); +} +.show-dialog-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 12px 14px 12px 18px; + border-bottom: 1px solid var(--hair); +} +.show-dialog-meta { + display: inline-flex; + align-items: center; + gap: 10px; + font-family: var(--mono); + font-size: 13px; + min-width: 0; + overflow: hidden; +} +.show-dialog-bench { + color: #fff; + white-space: nowrap; +} +.show-dialog-sep { + color: var(--muted); +} +.show-dialog-rskill { + color: var(--text-2); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.show-close { + flex: 0 0 auto; + width: 40px; + height: 40px; + display: grid; + place-items: center; + color: var(--text); + background: var(--bg-2); + border: 1px solid var(--hair-2); + border-radius: 9px; + cursor: pointer; + transition: color 0.2s, border-color 0.2s, background 0.2s; +} +.show-close:hover { + color: #fff; + border-color: var(--muted); + background: rgba(255, 255, 255, 0.04); +} +.show-close svg { + width: 20px; + height: 20px; +} +.show-dialog-video { + display: block; + width: 100%; + max-height: calc(90vh - 65px); + object-fit: contain; + background: #000; +} + +@media (max-width: 600px) { + .show-card { + width: clamp(200px, 70vw, 260px); + } +} diff --git a/src/components/VideoShowcase.jsx b/src/components/VideoShowcase.jsx new file mode 100644 index 0000000..b289d86 --- /dev/null +++ b/src/components/VideoShowcase.jsx @@ -0,0 +1,244 @@ +import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { motion, AnimatePresence, useReducedMotion } from "framer-motion"; +import { useReveal } from "../hooks/useReveal.js"; +import "./VideoShowcase.css"; + +/* ------------------------------------------------------------------ + Auto-discovery. + + Drop a clip into one of: + src/videos/benchmarks/ src/videos/simulation/ src/videos/deployment/ + + Name it __.mp4 — e.g. + libero-spatial_pi05_success.mp4 + warehouse-pick_rtdetr-v2_fail.mp4 + + The category comes from the folder; the overlays (benchmark name, + rSkill name, SUCCESS/FAIL) are parsed from the filename. Use hyphens + inside a name, underscores only as the three separators. No code + change is needed to add a clip — Vite globs the folders at build time. + ------------------------------------------------------------------ */ +const FILES = import.meta.glob("/src/videos/*/*.{mp4,webm,mov,m4v}", { + eager: true, + query: "?url", + import: "default", +}); + +function parseClip(path, url) { + const seg = path.split("/"); + const category = seg[seg.length - 2]; // folder name + const base = seg[seg.length - 1].replace(/\.[^.]+$/, ""); + const parts = base.split("_"); + const tail = parts[parts.length - 1] || ""; + const status = /^succ/i.test(tail) ? "success" : /^fail/i.test(tail) ? "fail" : null; + const benchmark = parts[0] || base; + const rskill = parts.slice(1, status ? -1 : undefined).join("_") || "—"; + return { id: path, url, category, benchmark, rskill, status }; +} + +const ALL = Object.entries(FILES).map(([path, url]) => parseClip(path, url)); + +const TABS = [ + { id: "benchmarks", label: "Benchmarks" }, + { id: "simulation", label: "Simulation" }, + { id: "deployment", label: "Deployment" }, +]; + +const ExpandIcon = (props) => ( + +); + +const CloseIcon = (props) => ( + +); + +/* One square, rounded, looping clip with its overlays. */ +function ClipCard({ clip, reduce, onOpen }) { + return ( + + ); +} + +/* Seamless horizontal marquee. Two copies of the track translate -50%; + the row pauses when any card is hovered/focused or the modal is open. + Under reduced-motion it becomes a plain horizontal scroll region. */ +function Marquee({ clips, reverse, paused, reduce, onOpen }) { + if (!clips.length) { + return ( +
+ Drop a clip into src/videos/{clips.category} named{" "} + <benchmark>_<rskill>_<success|fail>.mp4 and it appears here. +
+ ); + } + + const loop = reduce ? clips : [...clips, ...clips]; + const duration = Math.max(18, clips.length * 7); + + return ( +
+
    + {loop.map((clip, i) => ( +
  • = clips.length || undefined}> + +
  • + ))} +
+
+ ); +} + +/* Full-resolution modal — native aspect ratio, controls, sound. */ +function ClipModal({ clip, onClose }) { + const closeRef = useRef(null); + + useEffect(() => { + closeRef.current?.focus(); + const onKey = (e) => e.key === "Escape" && onClose(); + document.addEventListener("keydown", onKey); + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.removeEventListener("keydown", onKey); + document.body.style.overflow = prev; + }; + }, [onClose]); + + return createPortal( + + e.stopPropagation()} + initial={{ opacity: 0, scale: 0.96, y: 8 }} + animate={{ opacity: 1, scale: 1, y: 0 }} + exit={{ opacity: 0, scale: 0.97, y: 6 }} + transition={{ duration: 0.24, ease: [0.22, 1, 0.36, 1] }} + > +
+ + {clip.benchmark} + / + {clip.rskill} + {clip.status && ( + + + {clip.status === "success" ? "SUCCESS" : "FAIL"} + + )} + + +
+
+
, + document.body + ); +} + +export default function VideoShowcase() { + const head = useReveal(); + const reduce = useReducedMotion(); + const [active, setActive] = useState(TABS[0].id); + const [open, setOpen] = useState(null); + + const tabIndex = TABS.findIndex((t) => t.id === active); + const clips = ALL.filter((c) => c.category === active); + clips.category = active; // for the empty-state hint + + return ( +
+ +
03 — See it run
+

+ Every clip is a real run, scored and labelled. +

+

+ Benchmarks, simulation and on-hardware deployment — each video is one rSkill on one + benchmark, marked SUCCESS or FAIL exactly as the eval + recorded it. Hover to pause; click any clip to watch it full size. +

+
+ +
+ {TABS.map((t) => { + const count = ALL.filter((c) => c.category === t.id).length; + return ( + + ); + })} +
+ + + + + + + + + {open && setOpen(null)} />} + + + ); +} diff --git a/src/videos/README.md b/src/videos/README.md new file mode 100644 index 0000000..4ceecfc --- /dev/null +++ b/src/videos/README.md @@ -0,0 +1,43 @@ +# Showcase clips + +Videos shown in the **"06 — See it run"** section (`src/components/VideoShowcase.jsx`). +They are auto-discovered at build time — **no code change is needed to add one.** + +## How to add a clip + +1. Drop the file into the folder for its category: + - `benchmarks/` + - `simulation/` + - `deployment/` +2. Name it: + + ``` + __. + ``` + + Examples: + + ``` + benchmarks/libero-spatial_pi05_success.mp4 + simulation/warehouse-pick_rtdetr-v2_fail.mp4 + deployment/franka-stack_qwen35_success.mp4 + ``` + +That's it — it appears in the autoscroll, with the overlays filled in from the name. + +## Naming rules + +- **Three underscore-separated parts.** Use **hyphens** inside a name + (`libero-spatial`), never underscores — underscores are the separators. +- The **last** part must be `success` or `fail` (case-insensitive). It drives the + centre pill: a green blinking dot + `SUCCESS`, or a red dot + `FAIL`. +- Part 1 → the **top-left** chip (benchmark name). +- Part 2 → the **top-right** chip (rSkill name). +- Any resolution / aspect ratio is fine — clips are squared (centre-cropped) in + the strip, and shown at full native resolution in the modal. + +## Supported formats + +`.mp4`, `.webm`, `.mov`, `.m4v`. `.mp4` (H.264) is the safest cross-browser choice. +Keep strip clips reasonably small (they autoplay, muted, on loop); the modal plays +the same file with sound and controls. diff --git a/src/videos/benchmarks/.gitkeep b/src/videos/benchmarks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/videos/deployment/.gitkeep b/src/videos/deployment/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/videos/simulation/.gitkeep b/src/videos/simulation/.gitkeep new file mode 100644 index 0000000..e69de29 From b8f550951516c57c8f31647572e66f40a086a85f Mon Sep 17 00:00:00 2001 From: Adrian Llopart Date: Thu, 18 Jun 2026 15:25:52 +0200 Subject: [PATCH 02/24] fix(showcase): inline status in top bar, de-dupe carousel, randomize order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move the SUCCESS/FAIL pill into a top bar laid out as a 3-column grid (benchmark · status · rskill), so the status sits inline, centered between the two name chips instead of floating over the video center. - Only duplicate the strip when it actually overflows its container: the seamless-loop copy is then always off-screen, so a clip is never visible twice at once. When the clips fit, each is shown once, centered, with no scroll. Overflow is tracked with a ResizeObserver. - Randomize clip order once per page load (Fisher–Yates). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01PxVX8QDC2gmULaJxXN3tXu --- src/components/VideoShowcase.css | 79 ++++++++++++++----------- src/components/VideoShowcase.jsx | 99 ++++++++++++++++++++++++-------- 2 files changed, 118 insertions(+), 60 deletions(-) diff --git a/src/components/VideoShowcase.css b/src/components/VideoShowcase.css index e9492e1..d4e84f5 100644 --- a/src/components/VideoShowcase.css +++ b/src/components/VideoShowcase.css @@ -61,16 +61,21 @@ display: flex; gap: 16px; width: max-content; - list-style: none; padding: 6px 0; +} +.show-track.is-centered { + justify-content: center; + width: 100%; +} +.show-track.is-animated { animation: show-scroll var(--marquee-dur, 40s) linear infinite; } -.show-marquee[data-reverse] .show-track { +.show-track.is-animated[data-reverse] { animation-direction: reverse; } .show-track.is-paused, -.show-marquee:hover .show-track, -.show-marquee:focus-within .show-track { +.show-marquee:hover .show-track.is-animated, +.show-marquee:focus-within .show-track.is-animated { animation-play-state: paused; } @keyframes show-scroll { @@ -78,23 +83,27 @@ transform: translate3d(0, 0, 0); } to { - transform: translate3d(-50%, 0, 0); + transform: translate3d(calc(var(--marquee-shift) * -1), 0, 0); } } +.show-seq { + display: flex; + gap: 16px; + flex: 0 0 auto; + list-style: none; +} .show-item { flex: 0 0 auto; } -/* reduced-motion / no-JS-animation: a plain horizontal scroll region */ +/* reduced-motion: a plain, manually-scrollable strip */ .show-marquee.is-static { overflow-x: auto; - scroll-snap-type: x mandatory; } .show-marquee.is-static .show-track { animation: none; -} -.show-marquee.is-static .show-item { - scroll-snap-align: start; + justify-content: flex-start; + width: max-content; } /* ---- a clip card (square, rounded, with overlays) ---- */ @@ -125,20 +134,28 @@ object-fit: cover; display: block; } -/* legibility wash behind the overlays — top + center, fades out to clean video */ +/* legibility wash behind the top bar, fading out to clean video */ .show-grad { position: absolute; inset: 0; pointer-events: none; - background: linear-gradient(180deg, rgba(14, 15, 19, 0.62) 0%, rgba(14, 15, 19, 0) 34%), - radial-gradient(120% 70% at 50% 50%, rgba(14, 15, 19, 0.34) 0%, rgba(14, 15, 19, 0) 60%); + background: linear-gradient(180deg, rgba(14, 15, 19, 0.66) 0%, rgba(14, 15, 19, 0) 32%); } -/* corner metadata chips */ -.show-chip { +/* top bar: benchmark (left) · SUCCESS/FAIL (center) · rskill (right) */ +.show-top { position: absolute; top: 10px; - max-width: calc(50% - 14px); + left: 10px; + right: 10px; + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 8px; + pointer-events: none; +} +.show-chip { + min-width: 0; font-family: var(--mono); font-size: 11px; line-height: 1; @@ -153,41 +170,33 @@ overflow: hidden; text-overflow: ellipsis; } -.show-chip-tl { - left: 10px; +.show-chip-bench { + justify-self: start; color: #fff; } -.show-chip-tr { - right: 10px; +.show-chip-rskill { + justify-self: end; color: var(--text-2); } -/* center SUCCESS / FAIL pill */ +/* SUCCESS / FAIL pill — center of the top bar */ .show-status { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + justify-self: center; display: inline-flex; align-items: center; - gap: 8px; + gap: 7px; font-family: var(--mono); - font-size: 12px; + font-size: 11px; font-weight: 500; - letter-spacing: 0.16em; + letter-spacing: 0.14em; color: #fff; background: rgba(14, 15, 19, 0.62); border: 1px solid var(--accent-line); border-radius: 999px; - padding: 7px 14px; + padding: 5px 11px; + white-space: nowrap; backdrop-filter: blur(6px); } -.show-status.is-inline { - position: static; - transform: none; - font-size: 11px; - padding: 4px 10px; -} .show-dot { width: 8px; height: 8px; diff --git a/src/components/VideoShowcase.jsx b/src/components/VideoShowcase.jsx index b289d86..598b669 100644 --- a/src/components/VideoShowcase.jsx +++ b/src/components/VideoShowcase.jsx @@ -37,7 +37,17 @@ function parseClip(path, url) { return { id: path, url, category, benchmark, rskill, status }; } -const ALL = Object.entries(FILES).map(([path, url]) => parseClip(path, url)); +// Randomise clip order once per page load (Fisher–Yates). +function shuffle(arr) { + const a = arr.slice(); + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; +} + +const ALL = shuffle(Object.entries(FILES).map(([path, url]) => parseClip(path, url))); const TABS = [ { id: "benchmarks", label: "Benchmarks" }, @@ -68,15 +78,18 @@ function ClipCard({ clip, reduce, onOpen }) { autoPlay={!reduce} preload="metadata" tabIndex={-1} />