mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 17:16:46 +03:00
Add mobile layout for commands section
- New carousel + demo + info layout for commands on mobile (<900px) - Add touch support to framework viz (tap instead of hover) - Fix split-content width for demo comparisons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
61d26a7370
commit
8ef37b011c
+138
-15
@@ -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 {
|
||||
|
||||
@@ -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 = `
|
||||
<div style="
|
||||
font-family: var(--font-body);
|
||||
@@ -144,7 +148,7 @@ export class PeriodicTable {
|
||||
color: var(--color-ash);
|
||||
font-style: italic;
|
||||
">
|
||||
Hover over a command to see details
|
||||
${action} a command to see details
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -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' });
|
||||
|
||||
@@ -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) {
|
||||
<span class="terminal-title">zsh — 80x24</span>
|
||||
</div>
|
||||
<div class="terminal-body" id="terminal-content">
|
||||
<!-- Dynamic Content -->
|
||||
<div class="terminal-line">
|
||||
<span class="terminal-prompt">➜</span>
|
||||
<span>Waiting for input...</span>
|
||||
@@ -62,9 +97,8 @@ export function renderTerminalLayout(commands) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
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) =>
|
||||
`<span class="terminal-step">${i + 1}. ${step}...</span>`
|
||||
).join('<br>');
|
||||
|
||||
// Content update - no extra indentation to avoid whitespace
|
||||
container.innerHTML = `<div class="terminal-line"><span class="terminal-prompt">➜</span><span class="terminal-cmd">/${cmd.id}</span></div>
|
||||
<div class="terminal-output">${stepsOutput}<br><span class="terminal-done">✓ Complete</span></div>
|
||||
<div class="terminal-preview command-demo-area">${renderCommandDemo(cmd.id)}</div>
|
||||
<div class="terminal-line terminal-cursor-line"><span class="terminal-prompt">➜</span><span class="terminal-cursor"></span></div>`;
|
||||
|
||||
// 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) => `
|
||||
<button class="mobile-cmd-pill${i === 0 ? ' active' : ''}" data-id="${cmd.id}">
|
||||
/${cmd.id}
|
||||
</button>
|
||||
`).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 = `<div class="mobile-cmd-rel">↔ pairs with <code>/${relationship.pairs}</code></div>`;
|
||||
} else if (relationship.leadsTo && relationship.leadsTo.length > 0) {
|
||||
relationshipHTML = `<div class="mobile-cmd-rel">→ leads to ${relationship.leadsTo.map(c => `<code>/${c}</code>`).join(', ')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="mobile-cmd-info${i === 0 ? ' active' : ''}" data-id="${cmd.id}">
|
||||
<h3 class="mobile-cmd-name">/${cmd.id}</h3>
|
||||
<p class="mobile-cmd-desc">${cmd.description}</p>
|
||||
${relationshipHTML}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="mobile-commands-layout">
|
||||
<div class="mobile-carousel-wrapper">
|
||||
<div class="mobile-carousel">
|
||||
${carouselHTML}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-demo-area" id="mobile-demo-content">
|
||||
${renderCommandDemo(commands[0]?.id || 'audit')}
|
||||
</div>
|
||||
<div class="mobile-info-area">
|
||||
${infoCardsHTML}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user