diff --git a/public/css/main.css b/public/css/main.css index de143cc01..432fe4adb 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -630,6 +630,14 @@ code { z-index: 1; } +.split-content { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + .split-after { /* Angled clip-path matching divider's skewX(-10deg): top-right, bottom-left */ clip-path: polygon(78% 0%, 100% 0%, 100% 100%, 62% 100%); @@ -1202,6 +1210,131 @@ code { } } +/* ============================================ + MOBILE COMMANDS LAYOUT + ============================================ */ + +.mobile-commands-layout { + display: none; +} + +@media (max-width: 900px) { + .mobile-commands-layout { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + } + + .commands-container { + display: none; + } +} + +.mobile-carousel-wrapper { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + padding: var(--spacing-xs) 0; +} + +.mobile-carousel-wrapper::-webkit-scrollbar { + display: none; +} + +.mobile-carousel { + display: flex; + gap: var(--spacing-xs); + padding-right: var(--spacing-md); +} + +.mobile-cmd-pill { + flex-shrink: 0; + padding: var(--spacing-sm) var(--spacing-md); + min-height: 44px; + font-family: var(--font-mono); + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-ash); + background: var(--color-cream); + border: 1px solid var(--color-mist); + border-radius: 100px; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.mobile-cmd-pill:hover { + color: var(--color-charcoal); + border-color: var(--color-charcoal); +} + +.mobile-cmd-pill.active { + color: var(--color-paper); + background: var(--color-ink); + border-color: var(--color-ink); +} + +.mobile-demo-area { + background: var(--color-cream); + border: 1px solid var(--color-mist); + border-radius: 8px; + padding: var(--spacing-sm); +} + +.mobile-demo-area .demo-split-comparison { + width: 100%; +} + +.mobile-demo-area .split-container { + width: 100%; + max-width: 100%; + height: 320px; +} + +.mobile-demo-area .demo-caption { + font-size: 0.75rem; + margin-top: var(--spacing-sm); +} + +.mobile-info-area { + padding-top: var(--spacing-sm); +} + +.mobile-cmd-info { + display: none; + padding: var(--spacing-sm) 0; +} + +.mobile-cmd-info.active { + display: block; +} + +.mobile-cmd-name { + font-family: var(--font-mono); + font-size: 1.125rem; + font-weight: 600; + color: var(--color-ink); + margin: 0 0 var(--spacing-xs) 0; +} + +.mobile-cmd-desc { + font-size: 0.875rem; + color: var(--color-charcoal); + line-height: 1.5; + margin: 0; +} + +.mobile-cmd-rel { + margin-top: var(--spacing-xs); + font-size: 0.75rem; + color: var(--color-ash); +} + +.mobile-cmd-rel code { + font-family: var(--font-mono); + color: var(--color-ink); +} + /* ============================================ DOWNLOADS SECTION ============================================ */ @@ -1684,20 +1817,18 @@ code { flex-wrap: wrap; gap: var(--spacing-xs); margin-bottom: var(--spacing-lg); - border-bottom: 1px solid var(--color-mist); - padding-bottom: var(--spacing-sm); } .pattern-tab { background: none; border: none; + border-bottom: 2px solid transparent; font-family: var(--font-body); font-size: 0.875rem; color: var(--color-ash); padding: var(--spacing-xs) var(--spacing-sm); cursor: pointer; - transition: color var(--duration-fast) var(--ease-out); - position: relative; + transition: color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out); } .pattern-tab:hover { @@ -1707,16 +1838,7 @@ code { .pattern-tab.active { color: var(--color-ink); font-weight: 500; -} - -.pattern-tab.active::after { - content: ''; - position: absolute; - bottom: calc(-1 * var(--spacing-sm) - 1px); - left: 0; - right: 0; - height: 2px; - background: var(--color-accent); + border-bottom-color: var(--color-accent); } .pattern-tab:focus-visible { @@ -1829,9 +1951,10 @@ code { } .btn-small { - padding: var(--spacing-xs) var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-md); font-size: 0.8rem; gap: var(--spacing-xs); + min-height: 44px; /* Touch target minimum */ } .btn-small svg { diff --git a/public/js/components/framework-viz.js b/public/js/components/framework-viz.js index ee5bb3899..c7f428cdb 100644 --- a/public/js/components/framework-viz.js +++ b/public/js/components/framework-viz.js @@ -137,6 +137,10 @@ export class PeriodicTable { } showDefaultInfo() { + // Use "Tap" on touch devices, "Hover over" on desktop + const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + const action = isTouchDevice ? 'Tap' : 'Hover over'; + this.infoPanel.innerHTML = `
- Hover over a command to see details + ${action} a command to see details
`; } @@ -342,7 +346,7 @@ export class PeriodicTable { el.style.boxShadow = 'none'; }; - // Mouse events + // Mouse events (desktop) el.addEventListener('mouseenter', activate); el.addEventListener('mouseleave', deactivate); @@ -350,8 +354,15 @@ export class PeriodicTable { el.addEventListener('focus', activate); el.addEventListener('blur', deactivate); + // Touch events - activate on tap, stay active until another tap + el.addEventListener('touchstart', (e) => { + e.preventDefault(); // Prevent double-firing with click + activate(); + }, { passive: false }); + // Click/Enter to scroll to command el.addEventListener('click', () => { + activate(); // Also activate on click for touch devices const target = document.getElementById(`cmd-${cmd}`); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'center' }); diff --git a/public/js/components/glass-terminal.js b/public/js/components/glass-terminal.js index 8a0fce3b7..88673a87b 100644 --- a/public/js/components/glass-terminal.js +++ b/public/js/components/glass-terminal.js @@ -2,6 +2,16 @@ import { renderCommandDemo } from "../demo-renderer.js"; import { initSplitCompare } from "../effects/split-compare.js"; import { commandProcessSteps, commandCategories, commandRelationships } from "../data.js"; +// Track current split instance and command for cleanup +let currentSplitInstance = null; +let currentCommandId = null; + +const MOBILE_BREAKPOINT = 900; + +function isMobile() { + return window.innerWidth <= MOBILE_BREAKPOINT; +} + export function initGlassTerminal() { // Initial setup if needed } @@ -10,7 +20,34 @@ export function renderTerminalLayout(commands) { const container = document.querySelector('.commands-gallery'); if (!container) return; - // Group commands by category + if (isMobile()) { + renderMobileLayout(container, commands); + } else { + renderDesktopLayout(container, commands); + } + + // Re-render on resize crossing breakpoint + let wasMobile = isMobile(); + window.addEventListener('resize', () => { + const nowMobile = isMobile(); + if (nowMobile !== wasMobile) { + wasMobile = nowMobile; + currentSplitInstance = null; + currentCommandId = null; + if (nowMobile) { + renderMobileLayout(container, commands); + } else { + renderDesktopLayout(container, commands); + } + } + }); +} + +// ============================================ +// DESKTOP LAYOUT (unchanged) +// ============================================ + +function renderDesktopLayout(container, commands) { const categoryOrder = ['diagnostic', 'quality', 'intensity', 'adaptation', 'enhancement', 'system']; const categoryLabels = { 'diagnostic': 'Diagnose', @@ -28,7 +65,6 @@ export function renderTerminalLayout(commands) { grouped[cat].push(cmd); }); - // Build manual with category headers let manualHTML = ''; categoryOrder.forEach(cat => { if (grouped[cat] && grouped[cat].length > 0) { @@ -51,7 +87,6 @@ export function renderTerminalLayout(commands) { zsh — 80x24
-
Waiting for input... @@ -62,9 +97,8 @@ export function renderTerminalLayout(commands) {
`; - setupScrollSpy(commands); + setupDesktopScrollSpy(commands); - // Select first command initially if (commands.length > 0) { updateTerminal(commands[0], document.getElementById('terminal-content'), commands); const firstEntry = document.querySelector('.manual-entry'); @@ -95,19 +129,16 @@ function renderManualEntry(cmd) { `; } -function setupScrollSpy(commands) { +function setupDesktopScrollSpy(commands) { const entries = document.querySelectorAll('.manual-entry'); const terminalContent = document.getElementById('terminal-content'); - // Intersection Observer for Scroll Spy - trigger around vertical center const observer = new IntersectionObserver((observerEntries) => { observerEntries.forEach(entry => { if (entry.isIntersecting) { - // Update active state document.querySelectorAll('.manual-entry').forEach(e => e.classList.remove('active')); entry.target.classList.add('active'); - // Update terminal const cmdId = entry.target.dataset.id; const cmd = commands.find(c => c.id === cmdId); if (cmd) { @@ -117,15 +148,13 @@ function setupScrollSpy(commands) { }); }, { threshold: 0.1, - rootMargin: "-35% 0px -55% 0px" // Trigger zone biased toward upper-middle of viewport + rootMargin: "-35% 0px -55% 0px" }); entries.forEach(e => observer.observe(e)); - // Click interaction - immediate selection + smooth scroll entries.forEach(e => { e.addEventListener('click', () => { - // Immediately update state document.querySelectorAll('.manual-entry').forEach(el => el.classList.remove('active')); e.classList.add('active'); @@ -135,42 +164,32 @@ function setupScrollSpy(commands) { updateTerminal(cmd, terminalContent, commands); } - // Then scroll into view e.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); }); } -// Track current split instance and command for cleanup -let currentSplitInstance = null; -let currentCommandId = null; - function updateTerminal(cmd, container, allCommands) { if (!cmd || !container) return; - // Skip if already showing this command if (currentCommandId === cmd.id) return; currentCommandId = cmd.id; - // Cleanup previous split instance if (currentSplitInstance) { currentSplitInstance.destroy(); currentSplitInstance = null; } - // Get process steps for this command (or use generic fallback) const steps = commandProcessSteps[cmd.id] || ['Analyze', 'Transform', 'Verify']; const stepsOutput = steps.map((step, i) => `${i + 1}. ${step}...` ).join('
'); - // Content update - no extra indentation to avoid whitespace container.innerHTML = `
/${cmd.id}
${stepsOutput}
✓ Complete
${renderCommandDemo(cmd.id)}
`; - // Initialize split-compare effect for the demo const splitComparison = container.querySelector('.demo-split-comparison'); if (splitComparison) { currentSplitInstance = initSplitCompare(splitComparison, { @@ -182,4 +201,115 @@ function updateTerminal(cmd, container, allCommands) { } } +// ============================================ +// MOBILE LAYOUT - Carousel + Sticky Demo +// ============================================ +function renderMobileLayout(container, commands) { + // Build carousel pills + const carouselHTML = commands.map((cmd, i) => ` + + `).join(''); + + // Build command info cards (one per command, only active one shown) + const infoCardsHTML = commands.map((cmd, i) => { + 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(', ')}
`; + } + } + + return ` +
+

/${cmd.id}

+

${cmd.description}

+ ${relationshipHTML} +
+ `; + }).join(''); + + container.innerHTML = ` +
+ +
+ ${renderCommandDemo(commands[0]?.id || 'audit')} +
+
+ ${infoCardsHTML} +
+
+ `; + + setupMobileInteractions(commands); +} + +function setupMobileInteractions(commands) { + const pills = document.querySelectorAll('.mobile-cmd-pill'); + const demoArea = document.getElementById('mobile-demo-content'); + const infoCards = document.querySelectorAll('.mobile-cmd-info'); + + // Initialize first demo's split compare + const initialSplit = demoArea.querySelector('.demo-split-comparison'); + if (initialSplit) { + currentSplitInstance = initSplitCompare(initialSplit, { + defaultPosition: 50, + skewOffset: 6, + minPosition: 10, + maxPosition: 90 + }); + } + + // Pill click/tap handler + pills.forEach(pill => { + pill.addEventListener('click', () => { + const cmdId = pill.dataset.id; + const cmd = commands.find(c => c.id === cmdId); + if (!cmd || currentCommandId === cmdId) return; + + currentCommandId = cmdId; + + // Update active pill + pills.forEach(p => p.classList.remove('active')); + pill.classList.add('active'); + + // Scroll pill into view horizontally + pill.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); + + // Update info card + infoCards.forEach(card => { + card.classList.toggle('active', card.dataset.id === cmdId); + }); + + // Cleanup previous split + if (currentSplitInstance) { + currentSplitInstance.destroy(); + currentSplitInstance = null; + } + + // Update demo + demoArea.innerHTML = renderCommandDemo(cmdId); + + // Init new split compare + const splitComparison = demoArea.querySelector('.demo-split-comparison'); + if (splitComparison) { + currentSplitInstance = initSplitCompare(splitComparison, { + defaultPosition: 50, + skewOffset: 6, + minPosition: 10, + maxPosition: 90 + }); + } + }); + }); +}