more fixes

This commit is contained in:
Paul Bakaus
2025-12-15 16:55:21 -08:00
parent e9ebb5a4d9
commit 6987e6e160
9 changed files with 525 additions and 440 deletions
+59 -17
View File
@@ -30,30 +30,72 @@ async function loadContent() {
// Render commands (Glass Terminal)
renderTerminalLayout(allCommands);
// Render patterns and antipatterns
renderPatterns(patternsData.patterns, "patterns-grid", "pattern");
renderPatterns(patternsData.antipatterns, "antipatterns-grid", "antipattern");
// Render patterns with tabbed navigation
renderPatternsWithTabs(patternsData.patterns, patternsData.antipatterns);
} catch (error) {
console.error("Failed to load content:", error);
}
}
function renderPatterns(categories, containerId, classPrefix) {
const container = document.getElementById(containerId);
if (!container || !categories) return;
function renderPatternsWithTabs(patterns, antipatterns) {
const container = document.getElementById("patterns-categories");
if (!container || !patterns || !antipatterns) return;
container.innerHTML = categories
.map(
(category) => `
<div class="${classPrefix}-category">
<h3 class="${classPrefix}-category-title">${category.name}</h3>
<ul class="${classPrefix}-list">
${category.items.map((item) => `<li>${item}</li>`).join("")}
</ul>
</div>
`,
)
// Create a map of antipatterns by category name
const antipatternMap = {};
antipatterns.forEach(cat => {
antipatternMap[cat.name] = cat.items;
});
// Build tabs
const tabsHTML = patterns
.map((category, i) => `<button class="pattern-tab${i === 0 ? ' active' : ''}" data-tab="${category.name}">${category.name}</button>`)
.join("");
// Build panels
const panelsHTML = patterns
.map((category, i) => {
const antiItems = antipatternMap[category.name] || [];
return `
<div class="pattern-panel${i === 0 ? ' active' : ''}" data-panel="${category.name}">
<div class="pattern-columns">
<div class="pattern-column pattern-column--anti">
<span class="pattern-column-label">Don't</span>
<ul class="pattern-list">
${antiItems.map((item) => `<li class="pattern-item pattern-item--anti">${item}</li>`).join("")}
</ul>
</div>
<div class="pattern-column pattern-column--do">
<span class="pattern-column-label">Do</span>
<ul class="pattern-list">
${category.items.map((item) => `<li class="pattern-item pattern-item--do">${item}</li>`).join("")}
</ul>
</div>
</div>
</div>
`;
})
.join("");
container.innerHTML = `
<div class="pattern-tabs">${tabsHTML}</div>
<div class="pattern-panels">${panelsHTML}</div>
`;
// Tab click handling
container.querySelectorAll('.pattern-tab').forEach(tab => {
tab.addEventListener('click', () => {
const tabName = tab.dataset.tab;
// Update active tab
container.querySelectorAll('.pattern-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Update active panel
container.querySelectorAll('.pattern-panel').forEach(p => p.classList.remove('active'));
container.querySelector(`[data-panel="${tabName}"]`).classList.add('active');
});
});
}
// ============================================
+125 -158
View File
@@ -371,14 +371,15 @@ code {
gap: var(--spacing-xl);
}
.lens-comparison {
/* Split Screen Comparison */
.split-comparison {
position: relative;
width: 100%;
max-width: 600px;
margin: 0 auto;
}
.lens-container {
.split-container {
position: relative;
width: 100%;
max-width: 500px;
@@ -388,10 +389,12 @@ code {
overflow: hidden;
background: var(--color-cream);
border: 1px solid var(--color-mist);
cursor: ew-resize;
user-select: none;
}
.lens-before,
.lens-after {
.split-before,
.split-after {
position: absolute;
inset: 0;
display: flex;
@@ -399,46 +402,45 @@ code {
justify-content: center;
}
.lens-before {
.split-before {
z-index: 1;
}
.lens-after {
clip-path: circle(120px at var(--lens-x, 70%) var(--lens-y, 50%));
transition: clip-path 0.08s ease-out;
.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%);
z-index: 2;
background: var(--color-paper);
}
.lens-circle {
.split-divider {
position: absolute;
width: 240px;
height: 240px;
border: 2px solid var(--color-accent);
border-radius: 50%;
top: 0;
bottom: 0;
left: 70%;
width: 3px;
background: var(--color-accent);
transform: translateX(-50%) skewX(-10deg);
pointer-events: none;
z-index: 3;
left: var(--lens-x, 70%);
top: var(--lens-y, 50%);
transform: translate(-50%, -50%);
transition: left 0.08s ease-out, top 0.08s ease-out;
box-shadow: 0 0 0 4px var(--color-accent-dim), 0 20px 60px rgba(0,0,0,0.15);
box-shadow: 0 0 20px rgba(0,0,0,0.15);
}
.lens-label {
.split-label {
position: absolute;
top: -28px;
top: 50%;
left: 50%;
transform: translateX(-50%);
transform: translate(-50%, -50%) skewX(10deg);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.1em;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-accent);
background: var(--color-paper);
padding: 4px 12px;
color: var(--color-paper);
background: var(--color-accent);
padding: 6px 14px;
border-radius: 4px;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Slop Card - Generic AI output */
@@ -607,15 +609,15 @@ code {
background: var(--color-accent);
}
/* Lens Labels */
.lens-labels {
/* Split Labels */
.split-labels {
display: flex;
justify-content: center;
gap: var(--spacing-lg);
gap: var(--spacing-xl);
margin-top: var(--spacing-md);
}
.lens-label-item {
.split-label-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
@@ -623,14 +625,14 @@ code {
color: var(--color-ash);
}
.lens-label-dot {
.split-label-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-mist);
}
.lens-label-dot--accent {
.split-label-dot--accent {
background: var(--color-accent);
}
@@ -1322,168 +1324,133 @@ code {
margin: 0 auto;
}
.antidote-intro {
text-align: center;
/* Patterns - tabbed layout */
.patterns-categories {
margin-bottom: var(--spacing-xl);
}
.antidote-intro p {
font-size: 1.25rem;
/* Tab bar */
.pattern-tabs {
display: flex;
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;
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;
}
.pattern-tab:hover {
color: var(--color-charcoal);
max-width: 50ch;
margin: 0 auto;
}
.antidote-intro strong {
color: var(--color-accent);
font-weight: 600;
}
/* Section titles for patterns/antipatterns */
.patterns-section-title {
font-family: var(--font-display);
font-size: 1.5rem;
font-weight: 400;
.pattern-tab.active {
color: var(--color-ink);
margin: var(--spacing-lg) 0 var(--spacing-md);
text-align: center;
font-weight: 500;
}
.patterns-section-title::before {
content: '';
.pattern-tab.active::after {
content: '';
position: absolute;
bottom: calc(-1 * var(--spacing-sm) - 1px);
left: 0;
right: 0;
height: 2px;
background: var(--color-accent);
}
/* Panels */
.pattern-panel {
display: none;
}
.pattern-panel.active {
display: block;
}
.pattern-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-xl);
}
@media (max-width: 768px) {
.pattern-columns {
grid-template-columns: 1fr;
gap: var(--spacing-md);
}
}
.pattern-column-label {
display: block;
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: var(--spacing-sm);
color: var(--color-ash);
}
.pattern-column--anti .pattern-column-label {
color: var(--color-accent);
}
.pattern-column--do .pattern-column-label {
color: var(--color-success, #22c55e);
}
.patterns-section-title--anti::before {
content: '× ';
color: var(--color-accent);
}
/* Patterns grid (what TO do) */
.patterns-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.pattern-category {
background: var(--color-paper);
border: 1px solid var(--color-mist);
padding: var(--spacing-lg);
transition: all var(--duration-base) var(--ease-out);
}
.pattern-category:hover {
border-color: var(--color-success, #22c55e);
box-shadow: 0 10px 40px rgba(34, 197, 94, 0.1);
}
.pattern-category-title {
font-family: var(--font-display);
font-size: 1.25rem;
font-weight: 400;
color: var(--color-ink);
margin: 0 0 var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--color-mist);
}
/* Pattern list styles */
.pattern-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
gap: var(--spacing-xs);
}
.pattern-list li {
font-size: 0.875rem;
color: var(--color-charcoal);
.pattern-item {
font-size: 0.8125rem;
padding-left: var(--spacing-md);
position: relative;
line-height: 1.5;
}
.pattern-list li::before {
content: '✓';
position: absolute;
left: 0;
color: var(--color-success, #22c55e);
font-weight: 600;
font-size: 0.875rem;
/* Anti-pattern items - muted */
.pattern-item--anti {
color: var(--color-ash);
}
/* Anti-patterns grid */
.antipatterns-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.antipattern-category {
background: var(--color-paper);
border: 1px solid var(--color-mist);
padding: var(--spacing-lg);
transition: all var(--duration-base) var(--ease-out);
}
.antipattern-category:hover {
border-color: var(--color-accent);
box-shadow: 0 10px 40px var(--color-accent-dim);
}
.antipattern-category-title {
font-family: var(--font-display);
font-size: 1.25rem;
font-weight: 400;
color: var(--color-ink);
margin: 0 0 var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--color-mist);
}
.antipattern-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.antipattern-list li {
font-size: 0.875rem;
color: var(--color-charcoal);
padding-left: var(--spacing-md);
position: relative;
line-height: 1.5;
}
.antipattern-list li::before {
.pattern-item--anti::before {
content: '×';
position: absolute;
left: 0;
color: var(--color-accent);
font-weight: 600;
font-size: 1rem;
}
/* Antidote CTA */
.antidote-cta {
text-align: center;
padding-top: var(--spacing-lg);
border-top: 1px solid var(--color-mist);
}
.antidote-cta p {
font-size: 1.125rem;
/* Pattern items - prominent */
.pattern-item--do {
color: var(--color-charcoal);
max-width: 50ch;
margin: 0 auto;
font-style: italic;
}
.pattern-item--do::before {
content: '✓';
position: absolute;
left: 0;
color: var(--color-success, #22c55e);
font-weight: 600;
}
/* ============================================
+51 -87
View File
@@ -1,130 +1,94 @@
/**
* Problem Section - The Clarity Lens
* Interactive before/after comparison
* Problem Section - Split Screen Comparison
* Interactive before/after comparison with angled divider
*/
/* Additional lens styles that need complex CSS */
.lens-container {
cursor: crosshair;
user-select: none;
-webkit-user-select: none;
}
/* Subtle grid background for the lens container */
.lens-container::before {
/* Subtle grid background for the container */
.split-container::before {
content: '';
position: absolute;
inset: 0;
background-image:
background-image:
linear-gradient(var(--color-mist) 1px, transparent 1px),
linear-gradient(90deg, var(--color-mist) 1px, transparent 1px);
background-size: 20px 20px;
opacity: 0.5;
opacity: 0.3;
pointer-events: none;
}
/* Lens glow effect */
.lens-circle::before {
content: '';
/* Hover hint */
.split-container::after {
content: '← Drag →';
position: absolute;
inset: -20px;
border-radius: 50%;
background: radial-gradient(
circle,
var(--color-accent-dim) 0%,
transparent 70%
);
opacity: 0.5;
pointer-events: none;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
font-size: 0.625rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-ash);
background: var(--color-paper);
padding: 4px 12px;
border-radius: 4px;
opacity: 0.8;
transition: opacity 0.3s ease;
z-index: 10;
}
/* Pulsing animation for the lens when idle */
@keyframes lensGlow {
0%, 100% {
box-shadow:
0 0 0 4px var(--color-accent-dim),
0 20px 60px rgba(0,0,0,0.15);
}
50% {
box-shadow:
0 0 0 8px var(--color-accent-dim),
0 20px 80px rgba(0,0,0,0.2);
}
.split-container:hover::after {
opacity: 0;
}
.lens-container:not(:hover) .lens-circle {
animation: lensGlow 3s ease-in-out infinite;
}
/* Smooth transitions when hovering */
.lens-container:hover .lens-circle {
transition: left 0.05s linear, top 0.05s linear;
}
.lens-container:hover .lens-after {
transition: clip-path 0.05s linear;
}
/* Mobile: tap to reveal */
@media (hover: none) {
.lens-label {
display: none;
}
.lens-circle::after {
content: 'Tap & Drag';
position: absolute;
bottom: -36px;
left: 50%;
transform: translateX(-50%);
font-size: 0.625rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-accent);
background: var(--color-paper);
padding: 4px 12px;
border-radius: 4px;
white-space: nowrap;
}
}
/* Hover state for impeccable card inside lens */
.lens-after .impeccable-card {
/* Hover state for impeccable card */
.split-after .impeccable-card {
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
}
/* Entry animation for the lens */
@keyframes lensEntry {
/* Entry animation for the divider */
@keyframes splitEntry {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
transform: translateX(-50%) skewX(-10deg) scaleY(0.8);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
transform: translateX(-50%) skewX(-10deg) scaleY(1);
}
}
.lens-circle {
animation: lensEntry 0.6s var(--ease-out) 0.3s backwards;
.split-divider {
animation: splitEntry 0.6s var(--ease-out) 0.3s backwards;
}
/* Labels hover states */
.lens-label-item {
.split-label-item {
transition: color var(--duration-fast) var(--ease-out);
cursor: default;
}
.lens-label-item:hover {
.split-label-item:hover {
color: var(--color-text);
}
.lens-label-item[data-point="after"]:hover .lens-label-dot--accent {
.split-label-item[data-point="after"]:hover .split-label-dot--accent {
transform: scale(1.3);
}
.lens-label-dot {
.split-label-dot {
transition: transform var(--duration-fast) var(--ease-spring);
}
/* Mobile adjustments */
@media (hover: none) {
.split-container::after {
content: '← Swipe →';
}
}
@media (max-width: 600px) {
.split-label {
font-size: 0.5625rem;
padding: 4px 10px;
}
}
+64 -64
View File
@@ -243,89 +243,89 @@
margin-top: var(--spacing-sm) !important;
}
.terminal-preview .demo-container {
/* Demo Split Comparison in Terminal */
.terminal-preview .demo-split-comparison {
display: flex;
flex-direction: column;
height: 100%;
}
.terminal-preview .demo-header {
flex-shrink: 0;
padding: 0;
.terminal-preview .demo-split-comparison .split-container {
position: relative;
flex: 1;
min-height: 0;
overflow: hidden;
cursor: ew-resize;
user-select: none;
background: var(--color-cream);
}
.terminal-preview .demo-viewport {
flex: 1;
.terminal-preview .demo-split-comparison .split-before,
.terminal-preview .demo-split-comparison .split-after {
position: absolute;
inset: 0;
display: flex;
align-items: flex-start;
align-items: center;
justify-content: center;
padding: var(--spacing-md);
min-height: 0;
max-height: 320px;
overflow: auto;
overscroll-behavior: contain;
}
.terminal-preview .demo-viewport > * {
margin-block: auto; /* Center vertically when content is shorter than container */
.terminal-preview .demo-split-comparison .split-before {
z-index: 1;
background: var(--color-cream);
}
.terminal-preview .demo-caption {
.terminal-preview .demo-split-comparison .split-after {
z-index: 2;
background: var(--color-paper);
clip-path: polygon(58% 0%, 100% 0%, 100% 100%, 42% 100%);
}
.terminal-preview .demo-split-comparison .split-content {
width: 100%;
max-width: 280px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.terminal-preview .demo-split-comparison .split-divider {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 2px;
background: 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);
}
.terminal-preview .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(--color-accent);
padding: 4px 10px;
border-radius: 3px;
white-space: nowrap;
}
.terminal-preview .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);
}
/* Demo toggle in terminal */
.terminal-preview .demo-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
}
.terminal-preview .demo-toggle-switch {
width: 36px;
height: 20px;
background: var(--color-mist);
border-radius: 10px;
position: relative;
cursor: pointer;
transition: background 0.2s ease;
}
.terminal-preview .demo-toggle-switch::after {
content: '';
position: absolute;
top: 1px;
left: 1px;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
transition: transform 0.2s ease;
}
.terminal-preview .demo-toggle-switch.active {
background: var(--color-accent);
}
.terminal-preview .demo-toggle-switch.active::after {
transform: translateX(16px);
}
.terminal-preview .demo-toggle-label {
font-size: 0.75rem;
color: var(--color-ash);
transition: color 0.2s ease;
}
.terminal-preview .demo-toggle-label.active {
color: var(--color-ink);
font-weight: 500;
border-top: 1px solid var(--color-mist);
}
@keyframes blink { 50% { opacity: 0; } }
+17 -32
View File
@@ -34,7 +34,7 @@
<span class="hero-title-line" data-split-text>Impeccable</span>
<span class="hero-title-line hero-title-accent" data-split-text>Style</span>
</h1>
<p class="hero-subtitle" data-reveal>One design skill and 15 commands that work across Cursor, Claude Code, Gemini CLI, and Codex CLI. Stop getting generic AI output. Start getting considered design.</p>
<p class="hero-subtitle" data-reveal>One design skill and 15 commands that work across Cursor, Claude Code, Gemini CLI, and Codex CLI. Stop getting generic AI output. Start getting intentional design.</p>
<div class="hero-cta" data-reveal>
<a href="#downloads" class="btn btn-primary">Get the Framework</a>
</div>
@@ -104,10 +104,10 @@
</div>
</div>
<div class="lens-comparison" id="lens-comparison">
<div class="lens-container">
<div class="split-comparison" id="lens-comparison">
<div class="split-container">
<!-- Before: AI Slop -->
<div class="lens-before">
<div class="split-before">
<div class="slop-card">
<div class="slop-header">
<div class="slop-avatar"></div>
@@ -127,8 +127,8 @@
<span class="slop-callout" data-point="rounded">Cards on cards</span>
</div>
</div>
<!-- After: Impeccable (shown through lens) -->
<div class="lens-after">
<!-- After: Impeccable (shown through split) -->
<div class="split-after">
<div class="impeccable-card">
<p class="impeccable-eyebrow">Introducing</p>
<h3 class="impeccable-title">Thoughtful Design</h3>
@@ -136,18 +136,18 @@
<button class="impeccable-button">Explore</button>
</div>
</div>
<!-- The Lens -->
<div class="lens-circle" id="lens-circle">
<span class="lens-label">With Skills</span>
<!-- The Divider -->
<div class="split-divider">
<span class="split-label">With Skills</span>
</div>
</div>
<div class="lens-labels">
<div class="lens-label-item" data-point="before">
<span class="lens-label-dot"></span>
<div class="split-labels">
<div class="split-label-item" data-point="before">
<span class="split-label-dot"></span>
<span>Generic AI Output</span>
</div>
<div class="lens-label-item" data-point="after">
<span class="lens-label-dot lens-label-dot--accent"></span>
<div class="split-label-item" data-point="after">
<span class="split-label-dot split-label-dot--accent"></span>
<span>With Design Skills</span>
</div>
</div>
@@ -164,25 +164,10 @@
<div class="antidote-content">
<p class="section-lead" data-reveal>The original Claude frontend-design skill says to avoid Arial and Inter. We've expanded it with 60+ specific patterns that fight model bias across every design domain.</p>
<div class="antidote-intro" data-reveal>
<p>Your AI learns <strong>what TO do</strong> and <strong>what NOT to do</strong>. These rules are baked into the skill.</p>
</div>
<!-- Patterns (what TO do) -->
<h3 class="patterns-section-title" data-reveal>What TO Do</h3>
<div class="patterns-grid" id="patterns-grid" data-reveal>
<!-- Patterns by category with tabs -->
<div class="patterns-categories" id="patterns-categories">
<!-- Rendered by JS -->
</div>
<!-- Anti-patterns (what NOT to do) -->
<h3 class="patterns-section-title patterns-section-title--anti" data-reveal>What NOT to Do</h3>
<div class="antipatterns-grid" id="antipatterns-grid" data-reveal>
<!-- Rendered by JS -->
</div>
<div class="antidote-cta" data-reveal>
<p>These rules are embedded in the skill. Your AI internalizes them before writing a single line of code.</p>
</div>
</div>
</section>
@@ -295,7 +280,7 @@
<section class="platforms-section" id="downloads">
<div class="section-header" data-reveal>
<span class="section-number">05</span>
<h2 class="section-title">One Framework, Every AI Tool</h2>
<h2 class="section-title">One Framework, Every Agent Harness</h2>
<p class="section-subtitle">We wrote it once, then compiled it for each platform. Same design intelligence, native to your workflow.</p>
</div>
+25 -3
View File
@@ -1,5 +1,5 @@
import { renderCommandDemo } from "../demo-renderer.js";
import { setupCommandDemoToggles } from "../demo-toggles.js";
import { initSplitCompare } from "../effects/split-compare.js";
import { commandProcessSteps, commandCategories, commandRelationships } from "../data.js";
export function initGlassTerminal() {
@@ -141,9 +141,23 @@ function setupScrollSpy(commands) {
});
}
// 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) =>
@@ -156,6 +170,14 @@ function updateTerminal(cmd, container, allCommands) {
<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>`;
// Re-bind toggles
setupCommandDemoToggles(allCommands, () => {});
// Initialize split-compare effect for the demo
const splitComparison = container.querySelector('.demo-split-comparison');
if (splitComparison) {
currentSplitInstance = initSplitCompare(splitComparison, {
defaultPosition: 50,
skewOffset: 8,
minPosition: 5,
maxPosition: 95
});
}
}
+7 -57
View File
@@ -1,63 +1,13 @@
import { initSplitCompare } from "../effects/split-compare.js";
export function initLensEffect() {
const container = document.getElementById("lens-comparison");
if (!container) return;
const lensContainer = container.querySelector(".lens-container");
const lensAfter = container.querySelector(".lens-after");
const lensCircle = container.querySelector(".lens-circle");
if (!lensContainer || !lensAfter || !lensCircle) return;
let isHovering = false;
function updateLens(x, y) {
const rect = lensContainer.getBoundingClientRect();
const relX = ((x - rect.left) / rect.width) * 100;
const relY = ((y - rect.top) / rect.height) * 100;
const clampedX = Math.max(25, Math.min(75, relX));
const clampedY = Math.max(30, Math.min(70, relY));
lensAfter.style.clipPath = `circle(120px at ${clampedX}% ${clampedY}%)`;
lensCircle.style.setProperty("--lens-x", `${clampedX}%`);
lensCircle.style.setProperty("--lens-y", `${clampedY}%`);
lensCircle.style.left = `${clampedX}%`;
lensCircle.style.top = `${clampedY}%`;
}
lensContainer.addEventListener("mouseenter", () => {
isHovering = true;
initSplitCompare(container, {
defaultPosition: 70,
skewOffset: 8,
minPosition: 5,
maxPosition: 95
});
lensContainer.addEventListener("mouseleave", () => {
isHovering = false;
// Reset to default position
updateLens(
lensContainer.getBoundingClientRect().left +
lensContainer.offsetWidth * 0.7,
lensContainer.getBoundingClientRect().top +
lensContainer.offsetHeight * 0.5,
);
});
lensContainer.addEventListener("mousemove", (e) => {
if (isHovering) {
updateLens(e.clientX, e.clientY);
}
});
// Touch support
lensContainer.addEventListener("touchmove", (e) => {
e.preventDefault();
const touch = e.touches[0];
updateLens(touch.clientX, touch.clientY);
});
// Initialize at default position
updateLens(
lensContainer.getBoundingClientRect().left +
lensContainer.offsetWidth * 0.7,
lensContainer.getBoundingClientRect().top +
lensContainer.offsetHeight * 0.5,
);
}
+20 -22
View File
@@ -6,11 +6,11 @@ import { getCommandDemo } from './demos/commands/index.js';
import { getSkillDemo } from './demos/skills/index.js';
/**
* Render a command demo
* Render a command demo with split-screen comparison
*/
export function renderCommandDemo(commandId) {
const demo = getCommandDemo(commandId);
if (!demo) {
return `
<div class="demo-container">
@@ -22,20 +22,18 @@ export function renderCommandDemo(commandId) {
</div>
`;
}
const viewportClass = demo.viewportClass ? ` ${demo.viewportClass}` : '';
// Use split-screen comparison
return `
<div class="demo-container">
<div class="demo-header">
<div class="demo-toggle">
<span class="demo-toggle-label active">Before</span>
<div class="demo-toggle-switch" data-demo="command-${demo.id}"></div>
<span class="demo-toggle-label">After</span>
<div class="demo-split-comparison" data-demo="command-${demo.id}">
<div class="split-container">
<div class="split-before">
<div class="split-content">${demo.before}</div>
</div>
</div>
<div class="demo-viewport${viewportClass}" data-state="before" id="command-${demo.id}-viewport">
${demo.before}
<div class="split-after">
<div class="split-content">${demo.after || demo.before}</div>
</div>
<div class="split-divider"></div>
</div>
<div class="demo-caption">${demo.caption}</div>
</div>
@@ -47,7 +45,7 @@ export function renderCommandDemo(commandId) {
*/
export function renderSkillDemo(skillId) {
const skill = getSkillDemo(skillId);
if (!skill || !skill.tabs || skill.tabs.length === 0) {
return `
<div class="demo-container">
@@ -59,21 +57,21 @@ export function renderSkillDemo(skillId) {
</div>
`;
}
const showTabs = skill.tabs.length > 1;
const tabs = showTabs ? skill.tabs.map((tab, i) => `
<button class="demo-tab ${i === 0 ? 'active' : ''}" data-demo-tab="${tab.id}" data-skill="${skillId}">
${tab.label}
</button>
`).join('') : '';
const panels = skill.tabs.map((tab, i) => `
<div class="demo-panel ${i === 0 ? 'active' : ''}" data-demo-panel="${tab.id}">
${renderSkillTabDemo(skillId, tab)}
</div>
`).join('');
return `
<div class="demo-tabbed-container">
${showTabs ? `<div class="demo-tabs">${tabs}</div>` : ''}
@@ -90,7 +88,7 @@ export function renderSkillDemo(skillId) {
function renderSkillTabDemo(skillId, tab) {
const hasToggle = tab.hasToggle !== false;
const demoId = `${skillId}-${tab.id}`;
return `
<div class="demo-container">
<div class="demo-header">
@@ -118,10 +116,10 @@ export function setupDemoTabs() {
tab.addEventListener('click', () => {
const tabId = tab.dataset.demoTab;
const container = tab.closest('.demo-tabbed-container');
container.querySelectorAll('.demo-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
container.querySelectorAll('.demo-panel').forEach(p => p.classList.remove('active'));
container.querySelector(`[data-demo-panel="${tabId}"]`)?.classList.add('active');
});
+157
View File
@@ -0,0 +1,157 @@
// ============================================
// SPLIT COMPARE - Reusable before/after split-screen effect
// ============================================
/**
* Initialize split comparison effect on a container
* @param {HTMLElement} container - The container element with .split-container inside
* @param {Object} options - Configuration options
*/
export function initSplitCompare(container, options = {}) {
const {
defaultPosition = 70,
skewOffset = 8,
minPosition = 5,
maxPosition = 95,
lerpSpeed = 0.15,
animationThreshold = 40, // Re-trigger animations when crossing this threshold
onCrossThreshold = null // Callback when crossing threshold toward "after" side
} = options;
const splitContainer = container.querySelector('.split-container');
const splitAfter = container.querySelector('.split-after');
const splitDivider = container.querySelector('.split-divider');
if (!splitContainer || !splitAfter || !splitDivider) return null;
let isHovering = false;
let currentX = defaultPosition;
let targetX = defaultPosition;
let animationId = null;
let wasAboveThreshold = defaultPosition > animationThreshold;
function updateSplit(percent) {
const clampedX = Math.max(minPosition, Math.min(maxPosition, percent));
// Check if we crossed the threshold toward the "after" side (moving left)
const isAboveThreshold = clampedX > animationThreshold;
if (wasAboveThreshold && !isAboveThreshold) {
// Crossed threshold - re-trigger animations
retriggerAnimations();
if (onCrossThreshold) onCrossThreshold(clampedX);
}
wasAboveThreshold = isAboveThreshold;
// Angled clip-path matching divider's skewX(-10deg)
splitAfter.style.clipPath = `polygon(${clampedX + skewOffset}% 0%, 100% 0%, 100% 100%, ${clampedX - skewOffset}% 100%)`;
splitDivider.style.left = `${clampedX}%`;
}
function retriggerAnimations() {
// Find all elements with animations in the "after" content and re-trigger them
const afterContent = splitAfter.querySelector('.split-content');
if (afterContent) {
// Clone and replace to restart all CSS animations
const clone = afterContent.cloneNode(true);
afterContent.parentNode.replaceChild(clone, afterContent);
}
}
function animate() {
const diff = targetX - currentX;
if (Math.abs(diff) > 0.1) {
currentX += diff * lerpSpeed;
updateSplit(currentX);
animationId = requestAnimationFrame(animate);
} else {
currentX = targetX;
updateSplit(currentX);
animationId = null;
}
}
function startAnimation() {
if (!animationId) {
animationId = requestAnimationFrame(animate);
}
}
function handleMouseEnter() {
isHovering = true;
}
function handleMouseLeave() {
isHovering = false;
targetX = defaultPosition;
startAnimation();
}
function handleMouseMove(e) {
if (isHovering) {
const rect = splitContainer.getBoundingClientRect();
targetX = ((e.clientX - rect.left) / rect.width) * 100;
startAnimation();
}
}
function handleTouchStart() {
isHovering = true;
}
function handleTouchEnd() {
isHovering = false;
targetX = defaultPosition;
startAnimation();
}
function handleTouchMove(e) {
e.preventDefault();
const touch = e.touches[0];
const rect = splitContainer.getBoundingClientRect();
targetX = ((touch.clientX - rect.left) / rect.width) * 100;
startAnimation();
}
// Attach listeners
splitContainer.addEventListener('mouseenter', handleMouseEnter);
splitContainer.addEventListener('mouseleave', handleMouseLeave);
splitContainer.addEventListener('mousemove', handleMouseMove);
splitContainer.addEventListener('touchstart', handleTouchStart);
splitContainer.addEventListener('touchend', handleTouchEnd);
splitContainer.addEventListener('touchmove', handleTouchMove, { passive: false });
// Initialize
updateSplit(defaultPosition);
// Return cleanup function
return {
destroy() {
splitContainer.removeEventListener('mouseenter', handleMouseEnter);
splitContainer.removeEventListener('mouseleave', handleMouseLeave);
splitContainer.removeEventListener('mousemove', handleMouseMove);
splitContainer.removeEventListener('touchstart', handleTouchStart);
splitContainer.removeEventListener('touchend', handleTouchEnd);
splitContainer.removeEventListener('touchmove', handleTouchMove);
if (animationId) cancelAnimationFrame(animationId);
},
setPosition(percent) {
targetX = percent;
startAnimation();
}
};
}
/**
* Initialize all split comparisons on the page
*/
export function initAllSplitCompare(selector = '.split-comparison', options = {}) {
const containers = document.querySelectorAll(selector);
const instances = [];
containers.forEach(container => {
const instance = initSplitCompare(container, options);
if (instance) instances.push(instance);
});
return instances;
}