diff --git a/public/css/styles.css b/public/css/styles.css index 65504095f..11f3da7a9 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -100,6 +100,280 @@ .commands-gallery { display: block; } +.magazine-spread[data-category="diagnostic"] { + --spread-accent: oklch(55% 0.20 25); +} +.magazine-spread[data-category="quality"] { + --spread-accent: oklch(55% 0.20 160); +} +.magazine-spread[data-category="intensity"] { + --spread-accent: oklch(55% 0.22 350); +} +.magazine-spread[data-category="adaptation"] { + --spread-accent: oklch(55% 0.18 260); +} +.magazine-spread[data-category="enhancement"] { + --spread-accent: oklch(55% 0.20 300); +} +.magazine-spread[data-category="system"] { + --spread-accent: oklch(45% 0.05 0); +} +.magazine-container { + position: relative; + width: 100%; + display: grid; + grid-template-columns: 200px 1fr; + gap: var(--spacing-lg); + align-items: start; + overflow: hidden; +} +.fisheye-list { + position: relative; + height: 520px; + overflow: hidden; + mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent); + -webkit-mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent); +} +.fisheye-scroll { + height: 100%; + overflow-y: auto; + scrollbar-width: none; +} +.fisheye-scroll::-webkit-scrollbar { + display: none; +} +.fisheye-item { + display: block; + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 400; + color: var(--color-ink); + background: none; + border: none; + padding: 4px 0; + height: 39px; + cursor: pointer; + white-space: nowrap; + text-align: left; + width: 100%; + line-height: 1.3; + transform-origin: left center; + will-change: transform, opacity; +} +.fisheye-slash { + color: var(--color-mist); + font-weight: 300; +} +.fisheye-item.is-active { + font-weight: 600; +} +.fisheye-item.is-active .fisheye-slash { + color: var(--color-accent); +} +.magazine-viewport { + position: relative; + height: 520px; + overflow: hidden; +} +.magazine-spread { + position: absolute; + inset: 0; + display: grid; + grid-template-columns: 38% 1fr; + grid-template-rows: 1fr auto; + gap: 0 var(--spacing-xl); + padding: var(--spacing-lg) var(--spacing-lg); + opacity: 0; + pointer-events: none; + transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); + transform: translateY(16px); + will-change: opacity, transform; +} +.magazine-spread.active { + opacity: 1; + pointer-events: auto; + transform: translateY(0); + z-index: 2; +} +.magazine-spread.exiting { + opacity: 0; + transform: translateY(-16px); + z-index: 1; +} +.spread-identity { + grid-column: 1; + grid-row: 1 / 3; + display: flex; + flex-direction: column; + justify-content: center; + gap: var(--spacing-md); + padding-right: var(--spacing-lg); + border-right: 1px solid var(--color-mist); +} +.spread-category-label { + font-family: var(--font-body); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--spread-accent); +} +.spread-command-name { + font-family: var(--font-display); + font-size: clamp(3rem, 5.5vw, 5.5rem); + font-weight: 600; + line-height: 0.95; + color: var(--color-ink); + margin: 0; + letter-spacing: -0.02em; + word-break: break-word; + hyphens: auto; +} +.spread-command-name .spread-slash { + color: var(--spread-accent); + font-weight: 300; +} +.spread-command-name .beta-badge { + font-family: var(--font-body); + font-size: 0.55rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--spread-accent); + border: 1px solid var(--spread-accent); + border-radius: 3px; + padding: 2px 6px; + vertical-align: super; + margin-left: 8px; +} +.spread-description { + font-family: var(--font-body); + font-size: 0.9375rem; + line-height: 1.6; + color: var(--color-charcoal); + max-width: 360px; +} +.spread-flow { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + margin-top: var(--spacing-xs); +} +.spread-flow-label { + font-family: var(--font-body); + font-size: 0.75rem; + color: var(--color-ash); +} +.spread-flow-icon { + color: var(--spread-accent); + font-weight: 600; + font-size: 0.8125rem; +} +.spread-flow-cmd { + font-family: var(--font-mono); + font-size: 0.6875rem; + background: var(--color-mist); + padding: 2px 7px; + border-radius: 3px; + color: var(--color-ink); +} +.spread-demo-area { + grid-column: 2; + grid-row: 1 / 3; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} +.spread-demo-area .demo-split-comparison { + display: flex; + flex-direction: column; + height: 100%; + flex: 1; + min-height: 0; +} +.spread-demo-area .demo-split-comparison .split-container { + position: relative; + flex: 1; + min-height: 0; + overflow: hidden; + cursor: ew-resize; + user-select: none; + background: var(--color-cream); +} +.spread-demo-area .demo-split-comparison .split-before, .spread-demo-area .demo-split-comparison .split-after { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-md); +} +.spread-demo-area .demo-split-comparison .split-before { + z-index: 1; + background: var(--color-cream); +} +.spread-demo-area .demo-split-comparison .split-after { + z-index: 2; + background: var(--color-paper); + clip-path: polygon(58% 0%, 100% 0%, 100% 100%, 42% 100%); +} +.spread-demo-area .demo-split-comparison .split-content { + width: 100%; + max-width: 320px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.spread-demo-area .demo-split-comparison .split-divider { + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 2px; + background: var(--spread-accent, var(--color-accent)); + transform: translateX(-50%) skewX(-10deg); + pointer-events: none; + z-index: 3; + box-shadow: 0 0 12px rgba(0,0,0,0.1); +} +.spread-demo-area .demo-split-comparison .split-label { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) skewX(10deg); + font-size: 0.5625rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-paper); + background: var(--spread-accent, var(--color-accent)); + padding: 4px 10px; + border-radius: 3px; + white-space: nowrap; +} +.spread-demo-area .demo-split-comparison .demo-caption { + flex-shrink: 0; + font-size: 0.75rem; + color: var(--color-ash); + text-align: center; + padding: var(--spacing-sm) var(--spacing-md); + border-top: 1px solid var(--color-mist); + background: var(--color-paper); +} +.spread-demo-area .demo-container { + flex: 1; + display: flex; + flex-direction: column; +} +.spread-demo-area .demo-container .demo-viewport { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} .commands-container { display: grid; grid-template-columns: 1fr 1.2fr; @@ -314,6 +588,12 @@ font-style: italic; } @media (max-width: 900px) { + .magazine-container { + display: none; + } + .fisheye-list { + display: none; + } .glass-terminal-wrapper { display: none; } diff --git a/public/css/workflow.css b/public/css/workflow.css index f75d90171..348ac51ef 100644 --- a/public/css/workflow.css +++ b/public/css/workflow.css @@ -8,9 +8,327 @@ .commands-gallery { /* Reset grid */ - display: block; + display: block; } +/* ============================================ + MAGAZINE SPREAD - Desktop Layout + ============================================ */ + +/* Category accent colors */ +.magazine-spread[data-category="diagnostic"] { --spread-accent: oklch(55% 0.20 25); } +.magazine-spread[data-category="quality"] { --spread-accent: oklch(55% 0.20 160); } +.magazine-spread[data-category="intensity"] { --spread-accent: oklch(55% 0.22 350); } +.magazine-spread[data-category="adaptation"] { --spread-accent: oklch(55% 0.18 260); } +.magazine-spread[data-category="enhancement"] { --spread-accent: oklch(55% 0.20 300); } +.magazine-spread[data-category="system"] { --spread-accent: oklch(45% 0.05 0); } + +.magazine-container { + position: relative; + width: 100%; + display: grid; + grid-template-columns: 200px 1fr; + gap: var(--spacing-lg); + align-items: start; + overflow: hidden; +} + +/* Fisheye command list */ +.fisheye-list { + position: relative; + height: 520px; + overflow: hidden; + mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent); + -webkit-mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent); +} + +.fisheye-scroll { + height: 100%; + overflow-y: auto; + scrollbar-width: none; + /* Padding set dynamically by JS based on container height */ +} + +.fisheye-scroll::-webkit-scrollbar { display: none; } + +.fisheye-item { + display: block; + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 400; + color: var(--color-ink); + background: none; + border: none; + /* Fixed height: 1.5rem * 1.3 line-height = 31.2px + 8px padding = ~39px */ + padding: 4px 0; + height: 39px; + cursor: pointer; + white-space: nowrap; + text-align: left; + width: 100%; + line-height: 1.3; + transform-origin: left center; + will-change: transform, opacity; +} + +.fisheye-slash { + color: var(--color-mist); + font-weight: 300; +} + +.fisheye-item.is-active { + font-weight: 600; +} + +.fisheye-item.is-active .fisheye-slash { + color: var(--color-accent); +} + +/* The viewport that clips to one spread at a time */ +.magazine-viewport { + position: relative; + height: 520px; + overflow: hidden; +} + +/* Individual spread */ +.magazine-spread { + position: absolute; + inset: 0; + display: grid; + grid-template-columns: 38% 1fr; + grid-template-rows: 1fr auto; + gap: 0 var(--spacing-xl); + padding: var(--spacing-lg) var(--spacing-lg); + opacity: 0; + pointer-events: none; + transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); + transform: translateY(16px); + will-change: opacity, transform; +} + +.magazine-spread.active { + opacity: 1; + pointer-events: auto; + transform: translateY(0); + z-index: 2; +} + +.magazine-spread.exiting { + opacity: 0; + transform: translateY(-16px); + z-index: 1; +} + +/* Left column: Big name + meta */ +.spread-identity { + grid-column: 1; + grid-row: 1 / 3; + display: flex; + flex-direction: column; + justify-content: center; + gap: var(--spacing-md); + padding-right: var(--spacing-lg); + border-right: 1px solid var(--color-mist); +} + +.spread-category-label { + font-family: var(--font-body); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--spread-accent); +} + +.spread-command-name { + font-family: var(--font-display); + font-size: clamp(3rem, 5.5vw, 5.5rem); + font-weight: 600; + line-height: 0.95; + color: var(--color-ink); + margin: 0; + letter-spacing: -0.02em; + word-break: break-word; + hyphens: auto; +} + +.spread-command-name .spread-slash { + color: var(--spread-accent); + font-weight: 300; +} + +.spread-command-name .beta-badge { + font-family: var(--font-body); + font-size: 0.55rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--spread-accent); + border: 1px solid var(--spread-accent); + border-radius: 3px; + padding: 2px 6px; + vertical-align: super; + margin-left: 8px; +} + +.spread-description { + font-family: var(--font-body); + font-size: 0.9375rem; + line-height: 1.6; + color: var(--color-charcoal); + max-width: 360px; +} + +/* Flow / relationship line */ +.spread-flow { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + margin-top: var(--spacing-xs); +} + +.spread-flow-label { + font-family: var(--font-body); + font-size: 0.75rem; + color: var(--color-ash); +} + +.spread-flow-icon { + color: var(--spread-accent); + font-weight: 600; + font-size: 0.8125rem; +} + +.spread-flow-cmd { + font-family: var(--font-mono); + font-size: 0.6875rem; + background: var(--color-mist); + padding: 2px 7px; + border-radius: 3px; + color: var(--color-ink); +} + +/* Right column: Demo area */ +.spread-demo-area { + grid-column: 2; + grid-row: 1 / 3; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +/* Demo split comparison inside the spread */ +.spread-demo-area .demo-split-comparison { + display: flex; + flex-direction: column; + height: 100%; + flex: 1; + min-height: 0; +} + +.spread-demo-area .demo-split-comparison .split-container { + position: relative; + flex: 1; + min-height: 0; + overflow: hidden; + cursor: ew-resize; + user-select: none; + background: var(--color-cream); +} + +.spread-demo-area .demo-split-comparison .split-before, +.spread-demo-area .demo-split-comparison .split-after { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-md); +} + +.spread-demo-area .demo-split-comparison .split-before { + z-index: 1; + background: var(--color-cream); +} + +.spread-demo-area .demo-split-comparison .split-after { + z-index: 2; + background: var(--color-paper); + clip-path: polygon(58% 0%, 100% 0%, 100% 100%, 42% 100%); +} + +.spread-demo-area .demo-split-comparison .split-content { + width: 100%; + max-width: 320px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.spread-demo-area .demo-split-comparison .split-divider { + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 2px; + background: var(--spread-accent, var(--color-accent)); + transform: translateX(-50%) skewX(-10deg); + pointer-events: none; + z-index: 3; + box-shadow: 0 0 12px rgba(0,0,0,0.1); +} + +.spread-demo-area .demo-split-comparison .split-label { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) skewX(10deg); + font-size: 0.5625rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-paper); + background: var(--spread-accent, var(--color-accent)); + padding: 4px 10px; + border-radius: 3px; + white-space: nowrap; +} + +.spread-demo-area .demo-split-comparison .demo-caption { + flex-shrink: 0; + font-size: 0.75rem; + color: var(--color-ash); + text-align: center; + padding: var(--spacing-sm) var(--spacing-md); + border-top: 1px solid var(--color-mist); + background: var(--color-paper); +} + +/* Fallback demo container inside spread */ +.spread-demo-area .demo-container { + flex: 1; + display: flex; + flex-direction: column; +} + +.spread-demo-area .demo-container .demo-viewport { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +/* (nav moved to fisheye list) */ + +/* ============================================ + OLD DESKTOP STYLES (kept for mobile reuse) + ============================================ */ + .commands-container { display: grid; grid-template-columns: 1fr 1.2fr; @@ -264,9 +582,10 @@ } @media (max-width: 900px) { + .magazine-container { display: none; } + .fisheye-list { display: none; } .glass-terminal-wrapper { display: none; /* Hide on mobile for now, or stack */ - /* Alternatively: Position fixed bottom sheet */ } } diff --git a/public/js/components/glass-terminal.js b/public/js/components/glass-terminal.js index 55df7fc56..58af4a090 100644 --- a/public/js/components/glass-terminal.js +++ b/public/js/components/glass-terminal.js @@ -45,20 +45,41 @@ export function renderTerminalLayout(commands) { } // ============================================ -// DESKTOP LAYOUT (unchanged) +// DESKTOP LAYOUT - Magazine Spread // ============================================ -function renderDesktopLayout(container, commands) { - const categoryOrder = ['diagnostic', 'quality', 'intensity', 'adaptation', 'enhancement', 'system']; - const categoryLabels = { - 'diagnostic': 'Diagnose', - 'quality': 'Quality', - 'intensity': 'Intensity', - 'adaptation': 'Adaptation', - 'enhancement': 'Enhancement', - 'system': 'System' - }; +let magazineState = { + currentIndex: 0, + commands: [], + isTransitioning: false, + keyboardBound: false, + intersectionObserver: null +}; +const categoryOrder = ['diagnostic', 'quality', 'intensity', 'adaptation', 'enhancement', 'system']; +const categoryLabels = { + 'diagnostic': 'Diagnose', + 'quality': 'Quality', + 'intensity': 'Intensity', + 'adaptation': 'Adaptation', + 'enhancement': 'Enhancement', + 'system': 'System' +}; + +function renderDesktopLayout(container, commands) { + magazineState.commands = commands; + + // Determine starting index from URL hash + const hash = window.location.hash; + let startIndex = 0; + if (hash && hash.startsWith('#cmd-')) { + const cmdId = hash.slice(5); + const idx = commands.findIndex(c => c.id === cmdId); + if (idx >= 0) startIndex = idx; + } + magazineState.currentIndex = startIndex; + + // Build ordered list with category separators for nav const grouped = {}; commands.forEach(cmd => { const cat = commandCategories[cmd.id] || 'other'; @@ -66,64 +87,284 @@ function renderDesktopLayout(container, commands) { grouped[cat].push(cmd); }); - let manualHTML = ''; - categoryOrder.forEach(cat => { - if (grouped[cat] && grouped[cat].length > 0) { - manualHTML += `
${categoryLabels[cat] || cat}
`; - manualHTML += grouped[cat].map(cmd => renderManualEntry(cmd)).join(''); - } - }); + // Filter out deprecated shims + const deprecated = new Set(['teach-impeccable', 'frontend-design']); + const filteredCommands = commands.filter(c => !deprecated.has(c.id)); + magazineState.commands = filteredCommands; + + // Build spreads HTML + const spreadsHTML = filteredCommands.map((cmd, i) => renderSpread(cmd, i, i === startIndex)).join(''); + + // Build fisheye list + const fisheyeHTML = filteredCommands.map((cmd, i) => { + const cat = commandCategories[cmd.id] || 'other'; + return ``; + }).join(''); container.innerHTML = ` -
-
- ${manualHTML} +
+
+
${fisheyeHTML}
-
-
-
- - -
-
-
-
- command.md -
-
- Select a command to view source... -
-
-
-
-
-
- - - - zsh — 80x24 -
-
-
- - Waiting for input... -
-
-
-
-
+
+ ${spreadsHTML}
`; - setupStackTabs(); + // Init demo for active spread + initSpreadDemo(startIndex); - setupDesktopScrollSpy(commands); + // Set up interactions + setupFisheyeList(filteredCommands); + setupMagazineKeyboard(filteredCommands); + setupMagazineIntersection(container); +} - if (commands.length > 0) { - updateTerminal(commands[0], document.getElementById('terminal-content'), commands); - const firstEntry = document.querySelector('.manual-entry'); - if (firstEntry) firstEntry.classList.add('active'); +function renderSpread(cmd, index, isActive) { + const cat = commandCategories[cmd.id] || 'other'; + const isBeta = betaCommands.includes(cmd.id); + const relationship = commandRelationships[cmd.id]; + // Build relationship flow + let flowHTML = ''; + if (relationship) { + if (relationship.pairs) { + flowHTML = ` +
+ + pairs with + /${relationship.pairs} +
`; + } else if (relationship.leadsTo && relationship.leadsTo.length > 0) { + flowHTML = ` +
+ + leads to + ${relationship.leadsTo.map(c => `/${c}`).join(' ')} +
`; + } else if (relationship.combinesWith && relationship.combinesWith.length > 0) { + flowHTML = ` +
+ + + combines with + ${relationship.combinesWith.map(c => `/${c}`).join(' ')} +
`; + } + if (!flowHTML && relationship.flow) { + flowHTML = ` +
+ ${relationship.flow} +
`; + } + } + + return ` +
+
+ ${categoryLabels[cat] || cat} +

/${cmd.id}${isBeta ? 'BETA' : ''}

+

${cmd.description}

+ ${flowHTML} +
+
+ +
+
+ `; +} + +function initSpreadDemo(index) { + const cmd = magazineState.commands[index]; + if (!cmd) return; + + const spread = document.querySelector(`.magazine-spread[data-index="${index}"]`); + if (!spread) return; + + const demoArea = spread.querySelector('.spread-demo-area'); + if (!demoArea) return; + + // Cleanup previous split instance + if (currentSplitInstance) { + currentSplitInstance.destroy(); + currentSplitInstance = null; + } + + currentCommandId = cmd.id; + + // Only render HTML once; re-init split compare every time + if (demoArea.dataset.loaded !== 'true') { + demoArea.innerHTML = renderCommandDemo(cmd.id); + demoArea.dataset.loaded = 'true'; + } + + const splitComparison = demoArea.querySelector('.demo-split-comparison'); + if (splitComparison) { + currentSplitInstance = initSplitCompare(splitComparison, { + defaultPosition: 50 + }); + } + initCommandDemo(cmd.id, demoArea); +} + +function goToSpread(newIndex, commands) { + if (newIndex < 0 || newIndex >= commands.length) return; + if (newIndex === magazineState.currentIndex) return; + + const oldIndex = magazineState.currentIndex; + magazineState.currentIndex = newIndex; + + const spreads = document.querySelectorAll('.magazine-spread'); + + // Destroy the old split instance before switching + if (currentSplitInstance) { + currentSplitInstance.destroy(); + currentSplitInstance = null; + } + + // Mark old as exiting + spreads[oldIndex]?.classList.remove('active'); + spreads[oldIndex]?.classList.add('exiting'); + + // Mark new as active + spreads[newIndex]?.classList.add('active'); + spreads[newIndex]?.classList.remove('exiting'); + + // No fisheye sync here -- fisheye drives goToSpread, not the other way around + + // Update URL hash + const cmd = commands[newIndex]; + if (cmd) { + history.replaceState(null, '', `#cmd-${cmd.id}`); + } + + // Init demo for new spread (lazy) + initSpreadDemo(newIndex); + + // Clean exiting class after transition + setTimeout(() => { + spreads[oldIndex]?.classList.remove('exiting'); + }, 500); +} + +function setupFisheyeList(commands) { + const list = document.getElementById('fisheye-list'); + const scroll = list?.querySelector('.fisheye-scroll'); + const items = list ? [...list.querySelectorAll('.fisheye-item')] : []; + if (!list || !scroll || !items.length) return; + + // Fixed item height (matches CSS). All math is index-based. + const ITEM_H = 39; // px per item (font 1.5rem * 1.3 line-height + 4px*2 padding) + const RADIUS = 4; // items on each side affected by fisheye + const count = items.length; + let currentActive = -1; + + // Convert scroll position to fractional center index + const scrollToFractional = () => { + // Scroll area: padding-top puts index 0 at center when scrollTop=0 + return scroll.scrollTop / ITEM_H; + }; + + // Apply fisheye visual based on fractional center + const applyFisheye = (center) => { + items.forEach((item, i) => { + const dist = Math.abs(i - center); + const ratio = Math.max(0, 1 - dist / RADIUS); + // Strong ease-out for dramatic center pop + const eased = ratio * ratio * (3 - 2 * ratio); // smoothstep + const scale = 0.35 + eased * 0.65; + const opacity = 0.06 + eased * 0.94; + item.style.transform = `scale(${scale})`; + item.style.opacity = opacity; + }); + }; + + const activate = (idx) => { + idx = Math.max(0, Math.min(count - 1, Math.round(idx))); + if (idx === currentActive) return; + currentActive = idx; + items.forEach((it, i) => it.classList.toggle('is-active', i === idx)); + goToSpread(idx, commands); + }; + + const scrollToIndex = (idx, behavior = 'smooth') => { + idx = Math.max(0, Math.min(count - 1, idx)); + scroll.scrollTo({ top: idx * ITEM_H, behavior }); + }; + + // Scroll: update fisheye + activate nearest + let raf = null; + scroll.addEventListener('scroll', () => { + if (raf) cancelAnimationFrame(raf); + raf = requestAnimationFrame(() => { + const center = scrollToFractional(); + applyFisheye(center); + activate(Math.round(center)); + }); + }, { passive: true }); + + // Click to jump + items.forEach((item, i) => { + item.addEventListener('click', () => scrollToIndex(i)); + }); + + // Expose for keyboard/external nav + list._scrollToCommand = (idx) => scrollToIndex(idx); + + // Init + const startIdx = magazineState.currentIndex; + currentActive = -1; + // Set padding so index 0 sits at center and last index sits at center + const padH = list.clientHeight / 2 - ITEM_H / 2; + scroll.style.paddingTop = `${padH}px`; + scroll.style.paddingBottom = `${padH}px`; + scroll.scrollTop = startIdx * ITEM_H; + applyFisheye(startIdx); + activate(startIdx); +} + +function setupMagazineKeyboard(commands) { + if (magazineState.keyboardBound) return; + magazineState.keyboardBound = true; + + document.addEventListener('keydown', (e) => { + // Only respond when magazine is visible (desktop) + if (isMobile()) return; + const magazineEl = document.querySelector('.magazine-container'); + if (!magazineEl) return; + + // Check if magazine is somewhat in the viewport + const rect = magazineEl.getBoundingClientRect(); + const inView = rect.top < window.innerHeight && rect.bottom > 0; + if (!inView) return; + + const fisheyeList = document.getElementById('fisheye-list'); + if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { + e.preventDefault(); + fisheyeList?._scrollToCommand?.(magazineState.currentIndex + 1); + } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { + e.preventDefault(); + fisheyeList?._scrollToCommand?.(magazineState.currentIndex - 1); + } + }); +} + +function setupMagazineIntersection(container) { + // When the magazine section enters the viewport, ensure the active demo is rendered + if (magazineState.intersectionObserver) { + magazineState.intersectionObserver.disconnect(); + } + + magazineState.intersectionObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + initSpreadDemo(magazineState.currentIndex); + } + }); + }, { threshold: 0.1 }); + + const magazineEl = container.querySelector('.magazine-container'); + if (magazineEl) { + magazineState.intersectionObserver.observe(magazineEl); } } @@ -137,107 +378,6 @@ function truncateDescription(text, maxLen = 120) { return truncated.slice(0, lastSpace) + '...'; } -function renderManualEntry(cmd) { - const relationship = commandRelationships[cmd.id]; - let relationshipHTML = ''; - - if (relationship) { - if (relationship.pairs) { - relationshipHTML = `
pairs with /${relationship.pairs}
`; - } else if (relationship.leadsTo && relationship.leadsTo.length > 0) { - relationshipHTML = `
leads to ${relationship.leadsTo.map(c => `/${c}`).join(', ')}
`; - } else if (relationship.combinesWith && relationship.combinesWith.length > 0) { - relationshipHTML = `
+ combines with ${relationship.combinesWith.map(c => `/${c}`).join(', ')}
`; - } - } - - const isBeta = betaCommands.includes(cmd.id); - const shortDesc = truncateDescription(cmd.description); - - return ` -
-

/${cmd.id}${isBeta ? ' BETA' : ''}

-

${shortDesc}

- ${relationshipHTML} -
- `; -} - -function setupDesktopScrollSpy(commands) { - const entries = document.querySelectorAll('.manual-entry'); - const terminalContent = document.getElementById('terminal-content'); - - const observer = new IntersectionObserver((observerEntries) => { - observerEntries.forEach(entry => { - if (entry.isIntersecting) { - document.querySelectorAll('.manual-entry').forEach(e => e.classList.remove('active')); - entry.target.classList.add('active'); - - const cmdId = entry.target.dataset.id; - const cmd = commands.find(c => c.id === cmdId); - if (cmd) { - updateTerminal(cmd, terminalContent, commands); - history.replaceState(null, '', `#cmd-${cmdId}`); - } - } - }); - }, { - threshold: 0.1, - rootMargin: "-35% 0px -55% 0px" - }); - - entries.forEach(e => observer.observe(e)); - - entries.forEach(e => { - e.addEventListener('click', () => { - document.querySelectorAll('.manual-entry').forEach(el => el.classList.remove('active')); - e.classList.add('active'); - - const cmdId = e.dataset.id; - const cmd = commands.find(c => c.id === cmdId); - if (cmd) { - updateTerminal(cmd, terminalContent, commands); - history.replaceState(null, '', `#cmd-${cmdId}`); - } - - e.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }); - }); -} - -function updateTerminal(cmd, container, allCommands) { - if (!cmd || !container) return; - - if (currentCommandId === cmd.id) return; - currentCommandId = cmd.id; - - // Also update source content - updateSourceContent(cmd.id); - - if (currentSplitInstance) { - currentSplitInstance.destroy(); - currentSplitInstance = null; - } - - const steps = commandProcessSteps[cmd.id] || ['Analyze', 'Transform', 'Verify']; - const stepsOutput = steps.map((step, i) => - `${i + 1}. ${step}...` - ).join('
'); - - container.innerHTML = `
/${cmd.id}
-
${stepsOutput}
✓ Complete
-
${renderCommandDemo(cmd.id)}
-
`; - - const splitComparison = container.querySelector('.demo-split-comparison'); - if (splitComparison) { - currentSplitInstance = initSplitCompare(splitComparison, { - defaultPosition: 50 - }); - } - initCommandDemo(cmd.id, container); -} - // ============================================ // MOBILE LAYOUT - Carousel + Sticky Demo // ============================================