- 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>
164 lines
4.4 KiB
Text
164 lines
4.4 KiB
Text
---
|
||
/**
|
||
* 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>
|