222tuesday's picture
/* global React */const { useEffect, useRef, useState } = React;// ============================================================// DESIGN TOKENS — pinned to 1920×1080 slide canvas// ===================
88c1242 verified
// ============================================================
// SLIDE CANVAS — Presentation Kit
// Vanilla JS Slide Engine + Demo Slides
// ============================================================
// ----------------------------------------------------------
// PRIMITIVE HELPERS — Generate HTML strings
// ----------------------------------------------------------
const TYPE = {
eyebrow: 24, micro: 24, body: 32, bodyLg: 38, small: 26,
kicker: 44, title: 88, titleLg: 120, display: 200, mega: 280,
};
const C = {
orange: '#FF4D00', black: '#000000', white: '#FFFFFF',
white20: 'rgba(255,255,255,0.20)', white10: 'rgba(255,255,255,0.10)',
black20: 'rgba(0,0,0,0.20)', black10: 'rgba(0,0,0,0.10)',
};
function metaBar(left, right, extraStyle = '') {
return `<div class="meta-bar" style="${extraStyle}">
<span>${left}</span>
<span class="meta-right">${right}</span>
</div>`;
}
function bottomMeta(left, right, fg = C.white) {
return `<div class="bottom-meta" style="color:${fg}">
<span>${left}</span><span>${right}</span>
</div>`;
}
function display(text, size = 'title', color = 'inherit', extraStyle = '') {
return `<div class="display ${size}" style="color:${color};${extraStyle}">${text}</div>`;
}
function bodyText(text, size = 'size-body', extraStyle = '') {
return `<p class="body-text ${size}" style="${extraStyle}">${text}</p>`;
}
function mono(text, size = 'size-eyebrow', extraStyle = '') {
return `<span class="mono ${size}" style="${extraStyle}">${text}</span>`;
}
function idx(n, color = C.orange, size = TYPE.eyebrow) {
return `<span class="idx" style="font-size:${size}px;color:${color}">${String(n).padStart(2,'0')}</span>`;
}
function marquee(text, dir = 'left', bg = C.black, fg = C.white, skew = true, fontSize = '10vw') {
const items = Array(6).fill(text);
const itemsHtml = items.map(t =>
`<span class="marquee-item" style="font-size:${fontSize};color:${fg}">${t}<span class="diamond">◆</span></span>`
).join('');
return `<div class="marquee-strip ${skew ? 'skew' : ''}" style="background:${bg}">
<div class="marquee-track dir-${dir}">${itemsHtml}${itemsHtml}</div>
</div>`;
}
function badge(text, size = 180, fg = C.white, spinning = true) {
const r = size / 2 - 18;
const chars = text;
const repeated = `${chars} · ${chars} · ${chars} · `;
return `<div class="badge ${spinning ? 'spinning' : ''}" style="width:${size}px;height:${size}px">
<svg viewBox="0 0 ${size} ${size}">
<defs>
<path id="bp-${size}" d="M ${size/2},${size/2} m -${r},0 a ${r},${r} 0 1,1 ${r*2},0 a ${r},${r} 0 1,1 -${r*2},0"/>
</defs>
<circle cx="${size/2}" cy="${size/2}" r="${r+10}" fill="none" stroke="${fg}" stroke-width="2"/>
<text fill="${fg}" font-family="'Space Mono',monospace" font-size="24" letter-spacing="4">
<textPath href="#bp-${size}" startOffset="0%">${repeated}</textPath>
</text>
</svg>
</div>`;
}
function arrowSvg(size = 40, color = 'currentColor', rotate = 0) {
return `<svg class="arrow-svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter" style="transform:rotate(${rotate}deg)">
<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>
</svg>`;
}
// ----------------------------------------------------------
// SLIDE DATA
// ----------------------------------------------------------
const slides = [
// ---- SLIDE 0: Title / Cover ----
{
bg: C.orange,
fg: C.black,
html: `
<div style="flex:1;display:flex;flex-direction:column;justify-content:center;position:relative">
${metaBar('SLIDE CANVAS · 01', 'PRESENTATION KIT')}
<div class="anim-fade-in-up anim-delay-1">${display('SLIDE', 'titleLg', C.black)}</div>
<div class="anim-fade-in-up anim-delay-2" style="margin-top:8px">${display('CANVAS', 'mega', C.black)}</div>
<div style="margin-top:40px;max-width:680px" class="anim-fade-in-up anim-delay-3">
${bodyText('A design system for building bold, high-impact presentation slides. Primitives, tokens, and animations — ready to compose.', 'size-bodyLg')}
</div>
<div style="position:absolute;right:0;top:50%;transform:translateY(-50%)" class="anim-fade-in anim-delay-4">
${badge('SLIDE CANVAS · PRESENTATION KIT · ', 220, C.black, true)}
</div>
</div>
${bottomMeta('2024 · VANILLA JS', 'DESIGN TOKENS + PRIMITIVES', C.black)}
`
},
// ---- SLIDE 1: Marquee Divider ----
{
bg: C.black,
fg: C.white,
noPad: true,
html: `
<div style="flex:1;display:flex;flex-direction:column;justify-content:center;padding:0 var(--sp-pxLg)">
${display('DESIGN', 'display', C.white, 'margin-bottom:16px;')}
${display('TOKENS', 'mega', C.orange)}
</div>
${marquee('DESIGN TOKENS · TYPE · SPACE · COLOR · FONT', 'left', C.orange, C.black, true, '6vw')}
`
},
// ---- SLIDE 2: Design Tokens Overview ----
{
bg: C.black,
fg: C.white,
html: `
${metaBar('TOKENS · 02', 'TYPOGRAPHY + SPACE + COLOR')}
<div class="feature-grid anim-scale-in anim-delay-1">
<div class="feature-card">
<div class="card-idx">${idx(1)} ${mono('TYPE SCALE')}</div>
<div class="card-title">Typography</div>
<div class="card-body">10-step type scale from eyebrow (24px) to mega (280px). Display faces for impact, body faces for readability.</div>
<div style="margin-top:auto;display:flex;gap:8px;flex-wrap:wrap">
<span style="font-size:14px;color:${C.orange};font-family:var(--f-mono)">24</span>
<span style="font-size:18px;color:${C.orange};font-family:var(--f-mono)">32</span>
<span style="font-size:24px;color:${C.orange};font-family:var(--f-mono)">44</span>
<span style="font-size:32px;color:${C.orange};font-family:var(--f-mono)">88</span>
<span style="font-size:40px;color:${C.orange};font-family:var(--f-mono)">120</span>
</div>
</div>
<div class="feature-card">
<div class="card-idx">${idx(2)} ${mono('SPACING')}</div>
<div class="card-title">Space</div>
<div class="card-body">Consistent spacing tokens for padding, gaps, and layout rhythm. Slide padding, item gaps, and title spacing all tokenized.</div>
<div style="margin-top:auto;display:flex;flex-direction:column;gap:6px">
<div style="display:flex;align-items:center;gap:12px"><div style="width:32px;height:6px;background:${C.orange};border-radius:3px"></div><span style="font-family:var(--f-mono);font-size:14px;opacity:0.5">32px</span></div>
<div style="display:flex;align-items:center;gap:12px"><div style="width:56px;height:6px;background:${C.orange};border-radius:3px"></div><span style="font-family:var(--f-mono);font-size:14px;opacity:0.5">56px</span></div>
<div style="display:flex;align-items:center;gap:12px"><div style="width:80px;height:6px;background:${C.orange};border-radius:3px"></div><span style="font-family:var(--f-mono);font-size:14px;opacity:0.5">120px</span></div>
</div>
</div>
<div class="feature-card">
<div class="card-idx">${idx(3)} ${mono('COLOR')}</div>
<div class="card-title">Color</div>
<div class="card-body">Brand orange as the singular accent. Black, white, and transparency steps for layering and depth.</div>
<div style="margin-top:auto;display:flex;gap:8px">
<div style="width:40px;height:40px;border-radius:8px;background:${C.orange}" title="#FF4D00"></div>
<div style="width:40px;height:40px;border-radius:8px;background:${C.black};border:1px solid rgba(255,255,255,0.2)" title="#000000"></div>
<div style="width:40px;height:40px;border-radius:8px;background:${C.white}" title="#FFFFFF"></div>
<div style="width:40px;height:40px;border-radius:8px;background:rgba(255,255,255,0.20)" title="20%"></div>
<div style="width:40px;height:40px;border-radius:8px;background:rgba(255,255,255,0.10)" title="10%"></div>
</div>
</div>
</div>
`
},
// ---- SLIDE 3: Stats ----
{
bg: C.black,
fg: C.white,
html: `
${metaBar('IMPACT · 03', 'BY THE NUMBERS')}
<div style="flex:1;display:flex;flex-direction:column;justify-content:center">
<div class="stat-row anim-fade-in-up anim-delay-1" style="margin-bottom:64px">
<div class="stat-item">
<div class="stat-number" style="color:${C.orange}">10</div>
<div class="stat-label">Type Scale Steps</div>
</div>
<div class="stat-item">
<div class="stat-number">8</div>
<div class="stat-label">Spacing Tokens</div>
</div>
<div class="stat-item">
<div class="stat-number" style="color:${C.orange}">6</div>
<div class="stat-label">Color Tokens</div>
</div>
<div class="stat-item">
<div class="stat-number">9</div>
<div class="stat-label">UI Primitives</div>
</div>
</div>
<div class="anim-fade-in-up anim-delay-3" style="display:flex;flex-direction:column;gap:20px">
<div style="display:flex;justify-content:space-between;font-family:var(--f-mono);font-size:var(--t-eyebrow);text-transform:uppercase;letter-spacing:-0.02em"><span>Design Coverage</span><span style="color:${C.orange}">92%</span></div>
<div class="progress-bar"><div class="progress-fill" style="width:92%"></div></div>
<div style="display:flex;justify-content:space-between;font-family:var(--f-mono);font-size:var(--t-eyebrow);text-transform:uppercase;letter-spacing:-0.02em;margin-top:16px"><span>Token Adoption</span><span style="color:${C.orange}">87%</span></div>
<div class="progress-bar"><div class="progress-fill" style="width:87%"></div></div>
</div>
</div>
${bottomMeta('03 · IMPACT', 'DATA-DRIVEN DESIGN')}
`
},
// ---- SLIDE 4: Timeline ----
{
bg: C.black,
fg: C.white,
html: `
${metaBar('ROADMAP · 04', 'EVOLUTION')}
<div class="two-col">
<div style="display:flex;flex-direction:column;gap:24px" class="anim-fade-in-up anim-delay-1">
${display('THE', 'kicker')}
${display('ROAD-', 'titleLg')}
${display('MAP', 'titleLg', C.orange)}
${bodyText('A phased approach to building a complete design system for high-impact presentations.', 'size-body')}
</div>
<div class="timeline anim-fade-in-up anim-delay-2">
<div class="timeline-item">
<div class="timeline-year">PHASE 1</div>
<div class="timeline-content">
<div class="timeline-title">Tokens & Primitives</div>
<div class="timeline-desc">Define color, type, spacing, and build base UI components.</div>
</div>
</div>
<div class="timeline-item">
<div class="timeline-year">PHASE 2</div>
<div class="timeline-content">
<div class="timeline-title">Slide Templates</div>
<div class="timeline-desc">Compose primitives into reusable slide layouts: title, split, data, quote.</div>
</div>
</div>
<div class="timeline-item">
<div class="timeline-year">PHASE 3</div>
<div class="timeline-content">
<div class="timeline-title">Motion & Transitions</div>
<div class="timeline-desc">Add entrance animations, slide transitions, and micro-interactions.</div>
</div>
</div>
<div class="timeline-item">
<div class="timeline-year">PHASE 4</div>
<div class="timeline-content">
<div class="timeline-title">Export & Publish</div>
<div class="timeline-desc">Generate PDFs, share links, and present fullscreen from the browser.</div>
</div>
</div>
</div>
</div>
`
},
// ---- SLIDE 5: Primitives Showcase ----
{
bg: C.orange,
fg: C.black,
html: `
${metaBar('PRIMITIVES · 05', 'BUILDING BLOCKS', 'border-color:currentColor')}
<div style="flex:1;display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-gap)">
<div class="anim-fade-in-up anim-delay-1" style="display:flex;flex-direction:column;gap:16px">
<div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">METABAR</div>
<div style="border-bottom:2px solid currentColor;padding-bottom:12px;display:flex;justify-content:space-between;font-family:var(--f-mono);font-size:var(--t-eyebrow);letter-spacing:-0.02em;text-transform:uppercase"><span>SECTION · 05</span><span style="opacity:0.55">STATUS</span></div>
<div style="border-bottom:2px solid currentColor;padding-bottom:12px;display:flex;justify-content:space-between;font-family:var(--f-mono);font-size:var(--t-eyebrow);letter-spacing:-0.02em;text-transform:uppercase"><span>SECTION · 06</span><span style="opacity:0.55">META</span></div>
</div>
<div class="anim-fade-in-up anim-delay-2" style="display:flex;flex-direction:column;gap:16px">
<div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">DISPLAY</div>
<div class="display kicker">KICKER 44</div>
<div class="display title">TITLE 88</div>
<div class="display titleLg">BIG 120</div>
</div>
<div class="anim-fade-in-up anim-delay-3" style="display:flex;flex-direction:column;gap:16px">
<div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">BADGE</div>
<div style="display:flex;gap:24px;align-items:center;margin-top:8px">
${badge('PRIMITIVES · ', 140, C.black, true)}
${badge('DEMO · ', 110, C.black, true)}
</div>
</div>
<div class="anim-fade-in-up anim-delay-4" style="display:flex;flex-direction:column;gap:16px">
<div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">BODY TEXT</div>
<p class="body-text size-body">Body text at 32px for comfortable reading on large canvases.</p>
<p class="body-text size-small" style="opacity:0.65">Small body at 26px for secondary content and captions.</p>
</div>
<div class="anim-fade-in-up anim-delay-5" style="display:flex;flex-direction:column;gap:16px">
<div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">INDEX + ARROW</div>
<div style="display:flex;align-items:center;gap:16px;font-size:var(--t-title)">
<span class="idx" style="color:${C.black}">01</span> ${arrowSvg(40, C.black, 0)}
</div>
<div style="display:flex;align-items:center;gap:16px;font-size:var(--t-title)">
<span class="idx" style="color:${C.black}">02</span> ${arrowSvg(40, C.black, 0)}
</div>
</div>
<div class="anim-fade-in-up anim-delay-6" style="display:flex;flex-direction:column;gap:16px">
<div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">MONO LABEL</div>
<div class="mono size-eyebrow">UPPERCASE MONO 24</div>
<div class="mono size-micro" style="opacity:0.55">MICRO LABEL 24</div>
</div>
</div>
${bottomMeta('05 · PRIMITIVES', '9 COMPONENTS', C.black)}
`
},
// ---- SLIDE 6: Quote ----
{
bg: C.black,
fg: C.white,
html: `
<div style="flex:1;display:flex;align-items:center">
<div class="quote-block anim-fade-in-up anim-delay-1">
<div style="font-family:var(--f-display);font-size:var(--t-display);color:${C.orange};line-height:0.88;letter-spacing:-0.04em">"</div>
<div class="quote-text">DESIGN IS<br/>NOT JUST WHAT<br/>IT LOOKS LIKE.</div>
<div class="quote-attr">— STEVE JOBS · QUOTE SLIDE PRIMITIVE</div>
</div>
</div>
${bottomMeta('06 · PHILOSOPHY', 'QUOTE PRIMITIVE')}
`
},
// ---- SLIDE 7: Image + Text Split ----
{
bg: C.black,
fg: C.white,
html: `
${metaBar('SHOWCASE · 07', 'IMAGE + TEXT')}
<div class="two-col" style="flex:1">
<div class="anim-fade-in-up anim-delay-1">
${display('VISUAL', 'titleLg')}
${display('IMPACT', 'titleLg', C.orange)}
<div style="margin-top:24px">
${bodyText('Full-bleed images paired with bold typography create slides that command attention and communicate with clarity.', 'size-body')}
</div>
<div style="margin-top:32px;display:flex;align-items:center;gap:12px;color:${C.orange}">
${mono('EXPLORE MORE')} ${arrowSvg(24, C.orange, 0)}
</div>
</div>
<div class="img-block anim-scale-in anim-delay-2" style="height:100%;max-height:580px">
<img src="http://static.photos/abstract/1024x576/42" alt="Abstract visual" loading="lazy" />
</div>
</div>
${bottomMeta('07 · SHOWCASE', 'SPLIT LAYOUT')}
`
},
// ---- SLIDE 8: Marquee + End ----
{
bg: C.orange,
fg: C.black,
noPad: true,
html: `
${marquee('SLIDE CANVAS · PRESENTATION KIT · DESIGN SYSTEM · ', 'right', C.black, C.orange, true, '8vw')}
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;background:${C.orange};padding:0 var(--sp-pxLg)">
<div class="end-badge-container">
<div class="anim-scale-in anim-delay-1">
${badge('END · SLIDE CANVAS · PRESENTATION KIT · FIN · ', 260, C.black, true)}
</div>
</div>
<div class="anim-fade-in-up anim-delay-2" style="text-align:center;margin-top:40px">
${display('THANK YOU', 'titleLg', C.black)}
</div>
<div class="mono size-eyebrow anim-fade-in anim-delay-3" style="margin-top:16px;opacity:0.55;color:${C.black}">
BUILT WITH DESIGN TOKENS + PRIMITIVES
</div>
</div>
${marquee('THANK YOU · GRAZIE · MERCI · DANKE · ARIGATO · ', 'left', C.black, C.orange, true, '6vw')}
`
},
];
// ----------------------------------------------------------
// SLIDE ENGINE
// ----------------------------------------------------------
let currentSlide = 0;
let isTransitioning = false;
let overviewOpen = false;
function renderSlides() {
const container = document.getElementById('slide-container');
container.innerHTML = slides.map((s, i) => {
const padClass = s.noPad ? 'no-pad' : '';
return `<div class="slide ${padClass} ${i === 0 ? 'active' : ''}" data-slide="${i}"
style="background:${s.bg};color:${s.fg}">
${s.html}
</div>`;
}).join('');
}
function goToSlide(n, animate = true) {
if (isTransitioning || n < 0 || n >= slides.length) return;
isTransitioning = true;
const allSlides = document.querySelectorAll('.slide');
const prev = allSlides[currentSlide];
const next = allSlides[n];
if (prev) prev.classList.remove('active');
if (next) next.classList.add('active');
currentSlide = n;
updateIndicator();
setTimeout(() => { isTransitioning = false; }, animate ? 500 : 0);
}
function nextSlide() { goToSlide(currentSlide + 1); }
function prevSlide() { goToSlide(currentSlide - 1); }
function updateIndicator() {
const el = document.getElementById('slide-indicator');
el.textContent = `${String(currentSlide + 1).padStart(2, '0')} / ${String(slides.length).padStart(2, '0')}`;
}
// ----------------------------------------------------------
// OVERVIEW
// ----------------------------------------------------------
function toggleOverview() {
overviewOpen = !overviewOpen;
const modal = document.getElementById('overview-modal');
if (overviewOpen) {
buildOverview();
modal.classList.remove('hidden');
modal.classList.add('flex');
} else {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
}
function buildOverview() {
const grid = document.getElementById('overview-grid');
grid.innerHTML = slides.map((s, i) => {
const padClass = s.noPad ? 'no-pad' : '';
return `<div class="overview-card" onclick="overviewGo(${i})" title="Slide ${i+1}">
<div class="slide ${padClass}" style="background:${s.bg};color:${s.fg};position:relative;pointer-events:none;transform:scale(1);opacity:1;padding:24px 32px 20px;font-size:40%;overflow:hidden;aspect-ratio:16/9">
<div style="transform:scale(0.25);transform-origin:top left;width:400%;height:400%;pointer-events:none">
${s.html}
</div>
</div>
<div class="overview-label">${String(i+1).padStart(2,'0')}</div>
</div>`;
}).join('');
}
function overviewGo(n) {
toggleOverview();
setTimeout(() => goToSlide(n), 200);
}
// ----------------------------------------------------------
// FULLSCREEN
// ----------------------------------------------------------
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
} else {
document.exitFullscreen().catch(() => {});
}
}
// ----------------------------------------------------------
// KEYBOARD
// ----------------------------------------------------------
document.addEventListener('keydown', (e) => {
if (overviewOpen) {
if (e.key === 'Escape') toggleOverview();
return;
}
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
case ' ':
case 'PageDown':
e.preventDefault();
nextSlide();
break;
case 'ArrowLeft':
case 'ArrowUp':
case 'PageUp':
e.preventDefault();
prevSlide();
break;
case 'Home':
e.preventDefault();
goToSlide(0);
break;
case 'End':
e.preventDefault();
goToSlide(slides.length - 1);
break;
case 'f':
case 'F':
toggleFullscreen();
break;
case 'Escape':
if (document.fullscreenElement) toggleFullscreen();
break;
case 'o':
case 'O':
toggleOverview();
break;
}
});
// Touch support
let touchStartX = 0;
let touchStartY = 0;
document.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
}, { passive: true });
document.addEventListener('touchend', (e) => {
const dx = e.changedTouches[0].screenX - touchStartX;
const dy = e.changedTouches[0].screenY - touchStartY;
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
if (dx < 0) nextSlide();
else prevSlide();
}
}, { passive: true });
// Hide key hint after first navigation
let hintHidden = false;
function hideHint() {
if (hintHidden) return;
hintHidden = true;
const hint = document.getElementById('key-hint');
if (hint) hint.style.opacity = '0';
}
const origNext = nextSlide;
const origPrev = prevSlide;
// Override to also hide hint
window.nextSlide = function() { hideHint(); origNext(); };
window.prevSlide = function() { hideHint(); origPrev(); };
// ----------------------------------------------------------
// INIT
// ----------------------------------------------------------
function init() {
renderSlides();
updateIndicator();
// Auto-hide hint after 6s
setTimeout(() => { hideHint(); }, 6000);
}
document.addEventListener('DOMContentLoaded', init);