mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
Redesign Commands section: magazine spread + fisheye nav
Replace the old scrollspy + glass terminal layout with: - Editorial magazine spread: one command at a time with big display type name, category label, description, relationships, and before/after demo - Fisheye command list: vertical scrollable list with transform-based scale effect (0.4x to 1.0x), smooth center detection, synced to the active spread. Index-based math avoids layout oscillation. - Removed deprecated commands (teach-impeccable, frontend-design) from the browsable list - Removed transition lock that blocked rapid scrolling updates - Mobile layout preserved (existing carousel) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
17f389db45
commit
487ac0cf3a
@@ -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;
|
||||
}
|
||||
|
||||
+321
-2
@@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 += `<div class="command-category-header">${categoryLabels[cat] || cat}</div>`;
|
||||
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 `<button class="fisheye-item${i === startIndex ? ' is-active' : ''}" data-index="${i}" data-id="${cmd.id}" data-cat="${cat}"><span class="fisheye-slash">/</span>${cmd.id}</button>`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="commands-container">
|
||||
<div class="command-manual">
|
||||
${manualHTML}
|
||||
<div class="magazine-container">
|
||||
<div class="fisheye-list" id="fisheye-list">
|
||||
<div class="fisheye-scroll">${fisheyeHTML}</div>
|
||||
</div>
|
||||
<div class="glass-terminal-wrapper">
|
||||
<div class="terminal-stack">
|
||||
<div class="terminal-stack-tabs">
|
||||
<button class="terminal-stack-tab active" data-view="demo">Demo</button>
|
||||
<button class="terminal-stack-tab" data-view="source">Source</button>
|
||||
</div>
|
||||
<div class="terminal-window terminal-window--source">
|
||||
<div class="source-window">
|
||||
<div class="source-header">
|
||||
<span class="source-title" id="source-title">command.md</span>
|
||||
</div>
|
||||
<div class="source-body" id="source-content">
|
||||
<span class="source-loading">Select a command to view source...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-window terminal-window--demo">
|
||||
<div class="glass-terminal">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-dot red"></span>
|
||||
<span class="terminal-dot yellow"></span>
|
||||
<span class="terminal-dot green"></span>
|
||||
<span class="terminal-title">zsh — 80x24</span>
|
||||
</div>
|
||||
<div class="terminal-body" id="terminal-content">
|
||||
<div class="terminal-line">
|
||||
<span class="terminal-prompt">➜</span>
|
||||
<span>Waiting for input...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="magazine-viewport">
|
||||
${spreadsHTML}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="spread-flow">
|
||||
<span class="spread-flow-icon">↔</span>
|
||||
<span class="spread-flow-label">pairs with</span>
|
||||
<span class="spread-flow-cmd">/${relationship.pairs}</span>
|
||||
</div>`;
|
||||
} else if (relationship.leadsTo && relationship.leadsTo.length > 0) {
|
||||
flowHTML = `
|
||||
<div class="spread-flow">
|
||||
<span class="spread-flow-icon">→</span>
|
||||
<span class="spread-flow-label">leads to</span>
|
||||
${relationship.leadsTo.map(c => `<span class="spread-flow-cmd">/${c}</span>`).join(' ')}
|
||||
</div>`;
|
||||
} else if (relationship.combinesWith && relationship.combinesWith.length > 0) {
|
||||
flowHTML = `
|
||||
<div class="spread-flow">
|
||||
<span class="spread-flow-icon">+</span>
|
||||
<span class="spread-flow-label">combines with</span>
|
||||
${relationship.combinesWith.map(c => `<span class="spread-flow-cmd">/${c}</span>`).join(' ')}
|
||||
</div>`;
|
||||
}
|
||||
if (!flowHTML && relationship.flow) {
|
||||
flowHTML = `
|
||||
<div class="spread-flow">
|
||||
<span class="spread-flow-label">${relationship.flow}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="magazine-spread${isActive ? ' active' : ''}" data-index="${index}" data-category="${cat}" data-id="${cmd.id}" id="cmd-${cmd.id}">
|
||||
<div class="spread-identity">
|
||||
<span class="spread-category-label">${categoryLabels[cat] || cat}</span>
|
||||
<h3 class="spread-command-name"><span class="spread-slash">/</span>${cmd.id}${isBeta ? '<span class="beta-badge">BETA</span>' : ''}</h3>
|
||||
<p class="spread-description">${cmd.description}</p>
|
||||
${flowHTML}
|
||||
</div>
|
||||
<div class="spread-demo-area" data-demo-index="${index}">
|
||||
<!-- Demo rendered lazily -->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 = `<div class="manual-cmd-rel"><span class="rel-icon">↔</span> pairs with <code>/${relationship.pairs}</code></div>`;
|
||||
} else if (relationship.leadsTo && relationship.leadsTo.length > 0) {
|
||||
relationshipHTML = `<div class="manual-cmd-rel"><span class="rel-icon">→</span> leads to ${relationship.leadsTo.map(c => `<code>/${c}</code>`).join(', ')}</div>`;
|
||||
} else if (relationship.combinesWith && relationship.combinesWith.length > 0) {
|
||||
relationshipHTML = `<div class="manual-cmd-rel"><span class="rel-icon">+</span> combines with ${relationship.combinesWith.map(c => `<code>/${c}</code>`).join(', ')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
const isBeta = betaCommands.includes(cmd.id);
|
||||
const shortDesc = truncateDescription(cmd.description);
|
||||
|
||||
return `
|
||||
<div class="manual-entry" data-id="${cmd.id}" id="cmd-${cmd.id}">
|
||||
<h3 class="manual-cmd-name">/${cmd.id}${isBeta ? ' <span class="beta-badge">BETA</span>' : ''}</h3>
|
||||
<p class="manual-cmd-desc">${shortDesc}</p>
|
||||
${relationshipHTML}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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) =>
|
||||
`<span class="terminal-step">${i + 1}. ${step}...</span>`
|
||||
).join('<br>');
|
||||
|
||||
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>`;
|
||||
|
||||
const splitComparison = container.querySelector('.demo-split-comparison');
|
||||
if (splitComparison) {
|
||||
currentSplitInstance = initSplitCompare(splitComparison, {
|
||||
defaultPosition: 50
|
||||
});
|
||||
}
|
||||
initCommandDemo(cmd.id, container);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MOBILE LAYOUT - Carousel + Sticky Demo
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user