pcbjam/site/src/components/DemoVideo.astro
Viktor Vaczi 34ae85b2b5 fix(site): mobile waitlist input height, "Demo coming soon" panel, blog cleanup
- WaitlistForm: reset the input's flex when the form stacks — the 16rem
  flex-basis was applying to the height in column layout (256px-tall input).
- DemoVideo: add a comingSoon mode that shows a big "Demo coming soon" panel
  instead of the internal PRODUCE placeholder; enable it in the hero and the
  multiplayer section.
- Blog: retitle the post to "Porting KiCad's Graphics to WebGL with Claude"
  (clean slug), set body/description to "Coming soon.", and drop the
  placeholder text from the blog index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:15:02 +02:00

164 lines
4.4 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
/**
* DemoVideo — the multiplayer demo centerpiece.
*
* Poster-first and lazy (`preload="none"`); never autoplays in markup. A tiny
* IntersectionObserver (bound on astro:page-load, see <script> below) plays it
* when scrolled into view ONLY if the user hasn't asked for reduced motion —
* otherwise the static poster + manual play control stays.
*
* Until the real files exist, pass no `mp4`/`webm` and it renders a Placeholder.
*/
import Placeholder from './Placeholder.astro';
import Icon from './Icon.astro';
interface Props {
mp4?: string; // e.g. "/video/multiplayer-demo.mp4"
webm?: string;
poster?: string;
caption?: string;
ratio?: string; // default 16/9
/** When true and no video yet, show a big "Demo coming soon" panel instead of
* the internal PRODUCE placeholder. */
comingSoon?: boolean;
class?: string;
}
const {
mp4,
webm,
poster,
caption,
ratio = '16 / 9',
comingSoon = false,
class: cls = '',
} = Astro.props;
const hasVideo = Boolean(mp4 || webm);
---
<figure class={`demo ${cls}`}>
{
hasVideo ? (
<div class="demo-frame" data-demo-video style={`aspect-ratio:${ratio};`}>
<video
poster={poster}
preload="none"
muted
loop
playsinline
controls
aria-label={caption ?? 'PCBJam multiplayer editing demo'}
>
{webm && <source src={webm} type="video/webm" />}
{mp4 && <source src={mp4} type="video/mp4" />}
</video>
<button class="demo-play" type="button" aria-label="Play demo" data-demo-play>
<Icon name="icons/play.svg" size="3.5rem" alt="" />
</button>
</div>
) : comingSoon ? (
<div class="demo-soon bp-grid" style={`aspect-ratio:${ratio};`}>
<span class="demo-soon-text">Demo coming soon</span>
</div>
) : (
<Placeholder
label="PRODUCE: multiplayer demo loop — 2–3 live cursors, one board (§4b.2)"
kind="video"
ratio={ratio}
/>
)
}
{caption && <figcaption class="demo-caption">{caption}</figcaption>}
</figure>
<style>
.demo {
margin: 0;
}
.demo-frame {
position: relative;
width: 100%;
overflow: hidden;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--bg-soft);
}
.demo-frame video {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.demo-play {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border: 0;
background: color-mix(in srgb, var(--bg) 35%, transparent);
color: #fff;
cursor: pointer;
}
.demo-play[hidden] {
display: none;
}
.demo-soon {
display: grid;
place-items: center;
width: 100%;
padding: 1.5rem;
text-align: center;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background-color: var(--bg-soft);
}
.demo-soon-text {
font-size: clamp(1.6rem, 5.5vw, 3rem);
font-weight: 800;
letter-spacing: -0.01em;
color: var(--fg);
}
.demo-caption {
margin-top: 0.75rem;
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--fg-muted);
text-align: center;
}
</style>
<script>
// Bound on every load + SPA swap (ClientRouter re-runs astro:page-load).
function initDemoVideos() {
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll<HTMLElement>('[data-demo-video]').forEach((frame) => {
if (frame.dataset.bound === '1') return;
frame.dataset.bound = '1';
const video = frame.querySelector('video');
const playBtn = frame.querySelector<HTMLButtonElement>('[data-demo-play]');
if (!video) return;
playBtn?.addEventListener('click', () => {
video.play();
playBtn.hidden = true;
});
video.addEventListener('play', () => {
if (playBtn) playBtn.hidden = true;
});
if (reduce || !('IntersectionObserver' in window)) return; // poster + manual play only
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (e.isIntersecting) video.play().catch(() => {});
else video.pause();
}
},
{ threshold: 0.5 },
);
io.observe(frame);
});
}
document.addEventListener('astro:page-load', initDemoVideos);
</script>