mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 23:26:39 +03:00
refine(site): "The Case" crossfade + content cleanup, pattern-tabs scroll
- why-panel tab swap is a proper opacity+transform crossfade (display:grid stack area) instead of display:none jump; 650/800ms ease-out. - tab progress indicator animates linearly (timer, not eased). - Panel 01: dropped redundant "Every command reads this…" footer; moved the commands meta into the visual as a right-side sidebar aside PRODUCT.md. - Panel 02: "Browse the full catalog →" moved under the Gallery of Shame; pattern category tabs are now always a single-row horizontal scroll with JS-tracked edge-fade mask and chevron affordances; click centers the selected tab inside the strip (never scrolls the page). - Panel 04: "register" → "mode" in body, labels, meta for plain-language. - Panel 05: removed redundant "Works in Claude Code…" meta. - Panel 06: removed "Spec-compliant. Interoperable. Not a proprietary sidecar." meta. - .language-content grid gap reduced from --spacing-lg to --spacing-sm so the commands palette sits closer to the section lead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
2341fe3637
commit
3ffc485a8d
@@ -9,7 +9,7 @@
|
||||
<a href="/" data-nav="home">Home</a>
|
||||
<a href="/docs" data-nav="docs">Docs</a>
|
||||
<a href="/anti-patterns" data-nav="anti-patterns">Anti-Patterns</a>
|
||||
<a href="/docs/live" data-nav="live">Live</a>
|
||||
<a href="/live-mode" data-nav="live">Live</a>
|
||||
<a href="/visual-mode" data-nav="visual-mode">Overlay</a>
|
||||
</nav>
|
||||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
tagline: "Iterate on UI in the browser. Pick an element, drop a comment, get three variants. Accept one and it writes to source."
|
||||
---
|
||||
|
||||
<div class="docs-live-callout">
|
||||
<span class="docs-live-callout-icon" aria-hidden="true">▸</span>
|
||||
<span class="docs-live-callout-text">See it in action, with the animated demo, at <a href="/live-mode">/live-mode</a>. This page is the reference for what your AI harness reads when the command runs.</span>
|
||||
</div>
|
||||
|
||||
<div class="docs-viz-hero docs-viz-hero--plain">
|
||||
<div class="docs-viz-live-frame">
|
||||
<div class="docs-viz-live-chrome">
|
||||
|
||||
+31
-1
@@ -153,7 +153,10 @@ function renderPatternsWithTabs(patterns, antipatterns) {
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = `<div class="patterns-tabs">${tabsHTML}</div>${panelsHTML}`;
|
||||
container.innerHTML = `<div class="patterns-tabs-wrap"><div class="patterns-tabs" data-scroll="start">${tabsHTML}</div></div>${panelsHTML}`;
|
||||
|
||||
const tabsEl = container.querySelector('.patterns-tabs');
|
||||
const tabsWrap = container.querySelector('.patterns-tabs-wrap');
|
||||
|
||||
container.addEventListener('click', (e) => {
|
||||
const tab = e.target.closest('.patterns-tab');
|
||||
@@ -163,7 +166,34 @@ function renderPatternsWithTabs(patterns, antipatterns) {
|
||||
container.querySelectorAll('.patterns-content').forEach(p => p.classList.remove('is-active'));
|
||||
tab.classList.add('is-active');
|
||||
container.querySelector(`.patterns-content[data-index="${index}"]`).classList.add('is-active');
|
||||
// Center the clicked tab inside the tabs strip (not the page). Using
|
||||
// scrollBy on the container keeps the page scroll untouched.
|
||||
if (tabsEl) {
|
||||
const tabRect = tab.getBoundingClientRect();
|
||||
const stripRect = tabsEl.getBoundingClientRect();
|
||||
const offset = (tabRect.left + tabRect.width / 2) - (stripRect.left + stripRect.width / 2);
|
||||
tabsEl.scrollBy({ left: offset, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
// Track scroll position so the edge-fade mask only appears on sides where
|
||||
// there's actually more content. At the start, no left fade; at the end,
|
||||
// no right fade; if no overflow, no fade at all.
|
||||
const updateScrollState = () => {
|
||||
if (!tabsEl) return;
|
||||
const { scrollLeft, scrollWidth, clientWidth } = tabsEl;
|
||||
const max = scrollWidth - clientWidth;
|
||||
let state;
|
||||
if (max <= 1) state = 'none';
|
||||
else if (scrollLeft <= 1) state = 'start';
|
||||
else if (scrollLeft >= max - 1) state = 'end';
|
||||
else state = 'middle';
|
||||
tabsEl.dataset.scroll = state;
|
||||
if (tabsWrap) tabsWrap.dataset.scroll = state;
|
||||
};
|
||||
tabsEl?.addEventListener('scroll', updateScrollState, { passive: true });
|
||||
window.addEventListener('resize', updateScrollState);
|
||||
updateScrollState();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -1255,3 +1255,371 @@
|
||||
content: "✓";
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LIVE MODE LANDING PAGE (/live-mode)
|
||||
|
||||
Marketing-style landing that mirrors /visual-mode's structure.
|
||||
Hosts the animated homepage live-demo and surfaces the tutorial,
|
||||
reference, and install pathways.
|
||||
============================================ */
|
||||
|
||||
.live-mode-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(2.5rem, 5vw, 3.5rem);
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: clamp(2rem, 4vw, 3rem) clamp(1.25rem, 3vw, 2rem) clamp(4rem, 8vw, 6rem);
|
||||
}
|
||||
|
||||
.live-mode-page-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-width: 56ch;
|
||||
}
|
||||
|
||||
.live-mode-page-eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.live-mode-page-eyebrow-badge {
|
||||
padding: 2px 7px;
|
||||
border: 1px solid var(--color-accent);
|
||||
border-radius: 4px;
|
||||
font-size: 0.625rem;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.live-mode-page-title {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 400;
|
||||
font-size: clamp(2.5rem, 5vw, 3.5rem);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--color-ink);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.live-mode-page-lede {
|
||||
font-family: var(--font-body);
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.55;
|
||||
color: var(--color-charcoal);
|
||||
margin: 0;
|
||||
max-width: 56ch;
|
||||
}
|
||||
|
||||
.live-mode-start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
padding: 10px 14px;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
border-radius: 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875rem;
|
||||
align-self: flex-start;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.live-mode-start-prompt {
|
||||
color: var(--color-accent);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.live-mode-start-cmd {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.live-mode-start-cmd::-webkit-scrollbar { display: none; }
|
||||
|
||||
.live-mode-start-copy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
color: oklch(75% 0 0);
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.live-mode-start-copy:hover {
|
||||
background: oklch(22% 0 0);
|
||||
color: oklch(92% 0 0);
|
||||
}
|
||||
|
||||
.live-mode-start-copy.is-copied {
|
||||
color: oklch(70% 0.18 145);
|
||||
}
|
||||
|
||||
/* Demo section — wraps the animated homepage .live-demo block */
|
||||
.live-mode-demo-wrap {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.live-mode-demo-caption {
|
||||
margin: 14px 0 0;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-ash);
|
||||
line-height: 1.55;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Three-card "what happens" section */
|
||||
.live-mode-stages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.live-mode-stages-title {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-size: 1.75rem;
|
||||
color: var(--color-ink);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.live-mode-stages-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.live-mode-stages-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.live-mode-stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 18px;
|
||||
background: var(--color-paper);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.live-mode-stage-num {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--color-ash);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.live-mode-stage-name {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 1.375rem;
|
||||
color: var(--color-ink);
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.live-mode-stage-desc {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.55;
|
||||
color: var(--color-charcoal);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.live-mode-stage-viz {
|
||||
margin-top: 4px;
|
||||
padding: 14px;
|
||||
background: var(--color-cream);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 92px;
|
||||
}
|
||||
|
||||
/* Three pathway cards — tutorial, reference, install */
|
||||
.live-mode-pathways {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.live-mode-pathways-title {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-size: 1.75rem;
|
||||
color: var(--color-ink);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.live-mode-pathways-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.live-mode-pathways-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.live-mode-pathway {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 20px;
|
||||
background: var(--color-paper);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: border-color 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
.live-mode-pathway:hover {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.live-mode-pathway-kind {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.625rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.live-mode-pathway-title {
|
||||
font-family: var(--font-body);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.live-mode-pathway-desc {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-charcoal);
|
||||
line-height: 1.55;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.live-mode-pathway-cta {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* Frameworks strip */
|
||||
.live-mode-frameworks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 18px 20px;
|
||||
background: var(--color-cream);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.live-mode-frameworks-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--color-ash);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.live-mode-frameworks-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 14px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-ink);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.live-mode-frameworks-list li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.live-mode-frameworks-list li::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Docs/live page callout pointing back to /live-mode */
|
||||
.docs-live-callout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 clamp(1.25rem, 2.5vw, 1.75rem);
|
||||
background: var(--color-accent-dim);
|
||||
border: 1px solid var(--color-accent-soft);
|
||||
border-radius: 8px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-ink);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.docs-live-callout-icon {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-accent);
|
||||
color: var(--color-paper);
|
||||
border-radius: 50%;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.docs-live-callout-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.docs-live-callout-text a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,802 @@
|
||||
/* ============================================
|
||||
LIVE MODE — interactive demo loop
|
||||
============================================ */
|
||||
|
||||
.live-section {
|
||||
padding: var(--spacing-2xl) 0;
|
||||
border-top: 1px solid var(--color-mist);
|
||||
}
|
||||
|
||||
.live-content .section-lead {
|
||||
max-width: 64ch;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.live-demo {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
|
||||
gap: var(--spacing-xl);
|
||||
align-items: start;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
.live-demo-frame-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
min-width: 0;
|
||||
}
|
||||
@media (max-width: 920px) {
|
||||
.live-demo { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.live-demo-frame {
|
||||
position: relative;
|
||||
background: var(--color-cream);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16 / 9;
|
||||
max-width: 960px;
|
||||
box-shadow: 0 20px 50px oklch(0% 0 0 / 0.08);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.live-demo-chrome {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--color-mist);
|
||||
background: var(--color-paper);
|
||||
}
|
||||
.live-demo-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-mist);
|
||||
}
|
||||
.live-demo-url {
|
||||
margin-left: 14px;
|
||||
padding: 4px 14px;
|
||||
background: var(--color-cream);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 5px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
|
||||
.live-demo-stage {
|
||||
position: relative;
|
||||
height: calc(100% - 42px - 60px); /* minus chrome + bar */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Target card */
|
||||
.live-demo-target {
|
||||
position: relative;
|
||||
width: min(360px, 80%);
|
||||
min-height: 200px;
|
||||
}
|
||||
.live-demo-variant {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.99);
|
||||
transition: opacity 280ms var(--ease-out), transform 280ms var(--ease-out);
|
||||
pointer-events: none;
|
||||
}
|
||||
.live-demo-variant.is-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
.live-demo-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 22px 24px;
|
||||
background: var(--color-paper);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.live-demo-card-kicker {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-ash);
|
||||
}
|
||||
.live-demo-card h3 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
color: var(--color-ink);
|
||||
margin: 0;
|
||||
}
|
||||
.live-demo-card p {
|
||||
font-family: var(--font-body);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-charcoal);
|
||||
margin: 0;
|
||||
}
|
||||
.live-demo-card button {
|
||||
align-self: flex-start;
|
||||
margin-top: auto;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
padding: 9px 16px;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Per-variant look */
|
||||
.live-demo-card--plain {}
|
||||
.live-demo-card--v1 {
|
||||
background: var(--color-cream);
|
||||
}
|
||||
.live-demo-card--v1 h3 { font-style: italic; }
|
||||
.live-demo-card--v1 h3 em { color: var(--color-accent); font-style: italic; }
|
||||
.live-demo-card--v2 {
|
||||
background: var(--color-paper);
|
||||
border: 1px dashed var(--color-accent);
|
||||
}
|
||||
.live-demo-card--v2 .live-demo-card-kicker { color: var(--color-accent); }
|
||||
.live-demo-card--v2 h3 { font-family: var(--font-display); font-style: italic; }
|
||||
.live-demo-card--v2 button {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
.live-demo-card--v3 {
|
||||
background: oklch(96% 0.02 350);
|
||||
border: 1px solid var(--color-accent-soft);
|
||||
}
|
||||
.live-demo-card--v3 h3 em { color: var(--color-accent); font-style: italic; }
|
||||
.live-demo-card--v3 .live-demo-card-kicker { color: var(--color-accent-hover); }
|
||||
|
||||
/* Highlight outline over the target */
|
||||
.live-demo-outline {
|
||||
position: absolute;
|
||||
border: 2px solid var(--color-accent);
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms var(--ease-out), top 320ms var(--ease-out), left 320ms var(--ease-out), width 320ms var(--ease-out), height 320ms var(--ease-out);
|
||||
box-shadow: 0 0 0 4px var(--color-accent-dim);
|
||||
}
|
||||
.live-demo-outline.is-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Annotations */
|
||||
.live-demo-annotations {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 280ms var(--ease-out);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.live-demo-annotations.is-visible { opacity: 1; }
|
||||
.live-demo-stroke {
|
||||
position: absolute;
|
||||
width: 280px;
|
||||
height: 56px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -24px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.live-demo-stroke path {
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
.live-demo-annotations.is-visible .live-demo-stroke path {
|
||||
animation: liveDemoStroke 800ms var(--ease-out) forwards;
|
||||
}
|
||||
@keyframes liveDemoStroke {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
.live-demo-comment {
|
||||
position: absolute;
|
||||
top: 56%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
padding: 5px 10px;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms var(--ease-out);
|
||||
}
|
||||
.live-demo-annotations.is-comment-visible .live-demo-comment { opacity: 1; }
|
||||
|
||||
/* Simulated cursor */
|
||||
.live-demo-cursor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 18px;
|
||||
height: 22px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: translate(0, 0);
|
||||
transition: opacity 200ms var(--ease-out), transform 560ms var(--ease-out-quint);
|
||||
z-index: 5;
|
||||
filter: drop-shadow(0 2px 4px oklch(0% 0 0 / 0.2));
|
||||
}
|
||||
.live-demo-cursor.is-visible { opacity: 1; }
|
||||
.live-demo-cursor.is-click svg path {
|
||||
transform-origin: 4px 4px;
|
||||
animation: liveDemoCursorClick 220ms var(--ease-out);
|
||||
}
|
||||
@keyframes liveDemoCursorClick {
|
||||
0%, 100% { transform: scale(1); }
|
||||
40% { transform: scale(0.78); }
|
||||
}
|
||||
|
||||
/* Global bar — the persistent dark pill at the bottom of every session. */
|
||||
.live-demo-gbar {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-height: 36px;
|
||||
padding: 0 4px;
|
||||
background: oklch(14% 0 0);
|
||||
color: oklch(92% 0 0);
|
||||
border: 1px solid oklch(22% 0 0);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
box-shadow: 0 8px 24px oklch(0% 0 0 / 0.2);
|
||||
z-index: 4;
|
||||
}
|
||||
.live-demo-gbar-brand {
|
||||
font-family: var(--font-display);
|
||||
font-size: 16px;
|
||||
color: var(--color-accent);
|
||||
padding: 0 10px;
|
||||
}
|
||||
.live-demo-gbar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 10px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
color: oklch(75% 0 0);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.live-demo-gbar-btn.is-active {
|
||||
background: var(--color-accent-dim);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.live-demo-gbar-dmd {
|
||||
display: inline-grid;
|
||||
grid-template: repeat(2, 1fr) / repeat(2, 1fr);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.live-demo-gbar-dmd span:nth-child(1) { background: oklch(60% 0.25 350); }
|
||||
.live-demo-gbar-dmd span:nth-child(2) { background: oklch(60% 0.15 45); }
|
||||
.live-demo-gbar-dmd span:nth-child(3) { background: oklch(55% 0.12 250); }
|
||||
.live-demo-gbar-dmd span:nth-child(4) { background: oklch(30% 0 0); }
|
||||
.live-demo-gbar-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: oklch(28% 0 0);
|
||||
margin: 0 4px;
|
||||
}
|
||||
.live-demo-gbar-x {
|
||||
padding: 7px 10px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
color: oklch(60% 0 0);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Contextual bar — LIGHT paper-backed pill that floats near the picked
|
||||
element during a session. Matches live-browser.js's buildConfigureRow
|
||||
look (dark command pill, transparent input, ×N count, magenta Go). */
|
||||
.live-demo-ctx {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 6px);
|
||||
padding: 6px;
|
||||
background: var(--color-paper);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06);
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
color: var(--color-ink);
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity 260ms var(--ease-out), transform 260ms var(--ease-out);
|
||||
z-index: 3;
|
||||
max-width: 90%;
|
||||
}
|
||||
.live-demo-ctx[data-phase="configuring"],
|
||||
.live-demo-ctx[data-phase="generating"],
|
||||
.live-demo-ctx[data-phase="cycling"],
|
||||
.live-demo-ctx[data-phase="accepted"] {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
.live-demo-ctx-row {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.live-demo-ctx[data-phase="configuring"] .live-demo-ctx-row--configure,
|
||||
.live-demo-ctx[data-phase="generating"] .live-demo-ctx-row--generating,
|
||||
.live-demo-ctx[data-phase="cycling"] .live-demo-ctx-row--cycling,
|
||||
.live-demo-ctx[data-phase="accepted"] .live-demo-ctx-row--accepted {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Configure row: [delight ▾] [input] [×3] [Go →] */
|
||||
.live-demo-ctx-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 10px;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.live-demo-ctx-pill-caret { font-size: 9px; opacity: 0.7; margin-left: 2px; }
|
||||
.live-demo-ctx-input {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
padding: 5px 8px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.live-demo-ctx-caret {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 13px;
|
||||
background: var(--color-ink);
|
||||
margin-left: 2px;
|
||||
animation: liveDemoCaret 1s steps(1) infinite;
|
||||
}
|
||||
@keyframes liveDemoCaret {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
.live-demo-ctx-count {
|
||||
padding: 4px 6px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 5px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--color-ash);
|
||||
cursor: pointer;
|
||||
}
|
||||
.live-demo-ctx-go {
|
||||
padding: 5px 12px;
|
||||
background: var(--color-accent);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Generating row */
|
||||
.live-demo-ctx-row--generating {
|
||||
gap: 10px;
|
||||
padding: 4px 12px 4px 6px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
.live-demo-ctx-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid var(--color-mist);
|
||||
border-top-color: var(--color-accent);
|
||||
animation: liveDemoSpin 700ms linear infinite;
|
||||
}
|
||||
@keyframes liveDemoSpin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Cycling row */
|
||||
.live-demo-ctx-row--cycling { gap: 2px; padding: 2px; }
|
||||
.live-demo-ctx-nav {
|
||||
padding: 4px 10px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
color: var(--color-charcoal);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.live-demo-ctx-counter {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--color-ink);
|
||||
padding: 0 6px;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
.live-demo-ctx-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: var(--color-mist);
|
||||
margin: 0 4px;
|
||||
}
|
||||
.live-demo-ctx-discard {
|
||||
padding: 4px 10px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
color: var(--color-ash);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.live-demo-ctx-accept {
|
||||
padding: 5px 14px;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Accepted confirmation row */
|
||||
.live-demo-ctx-row--accepted {
|
||||
gap: 8px;
|
||||
padding: 6px 14px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
color: oklch(45% 0.18 145);
|
||||
}
|
||||
|
||||
/* Phase caption under the frame */
|
||||
.live-demo-caption {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-ash);
|
||||
}
|
||||
.live-demo-caption::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent);
|
||||
animation: liveDemoPulse 1.6s var(--ease-out) infinite;
|
||||
}
|
||||
@keyframes liveDemoPulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(0.7); }
|
||||
}
|
||||
.live-demo-caption-label { color: var(--color-ink); }
|
||||
|
||||
/* Supporting row under the demo */
|
||||
.live-demo-support {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
align-self: stretch;
|
||||
}
|
||||
.live-demo-support-cell {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px dashed var(--color-mist);
|
||||
}
|
||||
.live-demo-support-cell:last-child {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.live-demo-support-k {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.live-demo-support-v {
|
||||
font-family: var(--font-body);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.live-demo-support-v code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875em;
|
||||
background: var(--color-mist);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.live-demo-frame { aspect-ratio: 4 / 5; }
|
||||
.live-demo-target { width: 90%; }
|
||||
.live-demo-bar { font-size: 11px; min-height: 32px; }
|
||||
.live-demo-support { grid-template-columns: 1fr; gap: var(--spacing-md); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.live-demo-cursor,
|
||||
.live-demo-outline,
|
||||
.live-demo-variant,
|
||||
.live-demo-annotations,
|
||||
.live-demo-stroke path {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
.live-demo-caption::before { animation: none; }
|
||||
}
|
||||
/* ============================================
|
||||
LIVE MODE — skeleton page + richer variants
|
||||
============================================ */
|
||||
|
||||
/* Skeleton elements behind the target card — suggest "this is a real page" */
|
||||
.live-demo-skeleton {
|
||||
position: absolute;
|
||||
inset: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
filter: blur(0.3px);
|
||||
}
|
||||
.live-demo-skel-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--color-mist);
|
||||
}
|
||||
.live-demo-skel-logo {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
background: var(--color-charcoal);
|
||||
}
|
||||
.live-demo-skel-link {
|
||||
width: 48px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-mist);
|
||||
}
|
||||
.live-demo-skel-cta {
|
||||
margin-left: auto;
|
||||
width: 72px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background: var(--color-charcoal);
|
||||
}
|
||||
.live-demo-skel-heading {
|
||||
width: 60%;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
background: var(--color-mist);
|
||||
margin-top: 20px;
|
||||
}
|
||||
.live-demo-skel-line {
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-mist);
|
||||
}
|
||||
.live-demo-skel-line--short { width: 40%; }
|
||||
|
||||
/* Target stays above skeleton */
|
||||
.live-demo-target { position: relative; z-index: 1; }
|
||||
|
||||
/* ---- Card variants — sharper, more distinct ---- */
|
||||
|
||||
/* Variant 1: magazine column — italic serif, rule lines, off-white */
|
||||
.live-demo-card--v1 {
|
||||
background: var(--color-cream);
|
||||
padding: 18px 22px;
|
||||
border: 0;
|
||||
border-top: 3px solid var(--color-ink);
|
||||
border-radius: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
.live-demo-card--v1 .live-demo-card-kicker {
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.28em;
|
||||
}
|
||||
.live-demo-card--v1 h3 {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 28px;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.01em;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.live-demo-card--v1 h3 em { color: var(--color-accent); font-style: italic; }
|
||||
.live-demo-card--v1 p {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
.live-demo-card--v1 button {
|
||||
font-family: var(--font-body);
|
||||
align-self: flex-start;
|
||||
margin-top: 6px;
|
||||
background: transparent;
|
||||
color: var(--color-ink);
|
||||
border: 0;
|
||||
border-bottom: 1.5px solid var(--color-ink);
|
||||
padding: 4px 0;
|
||||
border-radius: 0;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
/* Variant 2: brutalist ticket — thick ink block, stamp glyph, mono */
|
||||
.live-demo-card--v2 {
|
||||
position: relative;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
padding: 20px 24px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
gap: 8px;
|
||||
}
|
||||
.live-demo-card--v2::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
background: var(--color-accent);
|
||||
}
|
||||
.live-demo-card-stamp {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 18px;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
color: var(--color-accent);
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
.live-demo-card--v2 .live-demo-card-kicker {
|
||||
color: var(--color-accent);
|
||||
letter-spacing: 0.3em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.live-demo-card--v2 h3 {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
line-height: 1.1;
|
||||
color: var(--color-paper);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.live-demo-card--v2 button {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
align-self: flex-start;
|
||||
margin-top: 6px;
|
||||
background: var(--color-accent);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 9px 14px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Variant 3: playful ticket — accent background, sticker stars, postcard vibe */
|
||||
.live-demo-card--v3 {
|
||||
position: relative;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, oklch(92% 0.08 350) 0, transparent 45%),
|
||||
var(--color-cream);
|
||||
border: 1px dashed var(--color-accent);
|
||||
border-radius: 10px;
|
||||
padding: 22px 24px 20px;
|
||||
gap: 8px;
|
||||
}
|
||||
.live-demo-card-sticker {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 14px;
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
color: var(--color-accent);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
transform: rotate(6deg);
|
||||
}
|
||||
.live-demo-card--v3 .live-demo-card-kicker {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-accent-hover);
|
||||
text-transform: none;
|
||||
}
|
||||
.live-demo-card--v3 h3 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 400;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.live-demo-card--v3 button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
align-self: flex-start;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 10px 18px;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: none;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
+68
-19
@@ -11,6 +11,7 @@
|
||||
/* Section partials (landing page only) */
|
||||
@import "./problem-section.css";
|
||||
@import "./workflow.css";
|
||||
@import "./live-mode.css";
|
||||
@import "./gallery.css";
|
||||
@import "./skill-demos.css";
|
||||
|
||||
@@ -1153,7 +1154,7 @@ code {
|
||||
|
||||
.language-content {
|
||||
display: grid;
|
||||
gap: var(--spacing-lg);
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Grid and flex items default to min-width: auto, which lets them expand to
|
||||
@@ -2442,28 +2443,76 @@ code {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.patterns-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
.patterns-tabs-wrap {
|
||||
position: relative;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.patterns-tabs {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin-left: calc(-1 * var(--spacing-lg));
|
||||
margin-right: calc(-1 * var(--spacing-lg));
|
||||
padding-left: var(--spacing-lg);
|
||||
padding-right: var(--spacing-lg);
|
||||
}
|
||||
.patterns-tabs {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
|
||||
.patterns-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.patterns-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Edge fade only on sides where more content exists. data-scroll is set
|
||||
by JS: start | middle | end | none. */
|
||||
.patterns-tabs[data-scroll="start"] {
|
||||
mask-image: linear-gradient(to right, black calc(100% - 40px), transparent);
|
||||
-webkit-mask-image: linear-gradient(to right, black calc(100% - 40px), transparent);
|
||||
}
|
||||
.patterns-tabs[data-scroll="middle"] {
|
||||
mask-image: linear-gradient(to right, transparent, black 32px, black calc(100% - 40px), transparent);
|
||||
-webkit-mask-image: linear-gradient(to right, transparent, black 32px, black calc(100% - 40px), transparent);
|
||||
}
|
||||
.patterns-tabs[data-scroll="end"] {
|
||||
mask-image: linear-gradient(to right, transparent, black 32px);
|
||||
-webkit-mask-image: linear-gradient(to right, transparent, black 32px);
|
||||
}
|
||||
.patterns-tabs[data-scroll="none"] {
|
||||
mask-image: none;
|
||||
-webkit-mask-image: none;
|
||||
}
|
||||
|
||||
/* Chevrons sit on the wrapper (outside the mask) so they're always fully
|
||||
opaque, regardless of edge fade. Visible only when scroll can progress
|
||||
in that direction. */
|
||||
.patterns-tabs-wrap::before,
|
||||
.patterns-tabs-wrap::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-top: 1.5px solid var(--color-ash);
|
||||
border-right: 1.5px solid var(--color-ash);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
.patterns-tabs-wrap::before {
|
||||
left: 2px;
|
||||
transform: translateY(-50%) rotate(-135deg);
|
||||
}
|
||||
.patterns-tabs-wrap::after {
|
||||
right: 2px;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
}
|
||||
.patterns-tabs-wrap[data-scroll="start"]::after,
|
||||
.patterns-tabs-wrap[data-scroll="middle"]::after { opacity: 1; }
|
||||
.patterns-tabs-wrap[data-scroll="end"]::before,
|
||||
.patterns-tabs-wrap[data-scroll="middle"]::before { opacity: 1; }
|
||||
|
||||
.patterns-tab {
|
||||
scroll-snap-align: start;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.patterns-tab {
|
||||
|
||||
+88
-816
@@ -1472,11 +1472,27 @@
|
||||
border-radius: 10px;
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: 0 1px 2px oklch(0.2 0 0 / 0.04);
|
||||
/* All panels stack in one grid cell so the outgoing panel can cross-fade
|
||||
out while the incoming one fades in. Container auto-sizes to the
|
||||
tallest panel, so switching between tabs doesn't jump. */
|
||||
display: grid;
|
||||
grid-template-areas: "stack";
|
||||
}
|
||||
|
||||
.why-panel {
|
||||
display: none;
|
||||
animation: whyFadeIn 260ms var(--ease-out);
|
||||
.why-panel,
|
||||
.why-panel[hidden] {
|
||||
/* [hidden] attribute on initial markup would force display:none and skip
|
||||
the crossfade; keep the element rendered so opacity/transform can run. */
|
||||
display: block;
|
||||
grid-area: stack;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(8px);
|
||||
transition:
|
||||
opacity 650ms var(--ease-out),
|
||||
transform 800ms var(--ease-out),
|
||||
visibility 0s linear 650ms;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.why-panel-title,
|
||||
@@ -1486,7 +1502,14 @@
|
||||
}
|
||||
|
||||
.why-panel.is-active {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
transition:
|
||||
opacity 650ms var(--ease-out),
|
||||
transform 800ms var(--ease-out),
|
||||
visibility 0s linear 0s;
|
||||
}
|
||||
|
||||
.why-panel-title {
|
||||
@@ -1557,13 +1580,12 @@
|
||||
|
||||
.why-panel-meta a:hover { color: var(--color-accent); }
|
||||
|
||||
@keyframes whyFadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.why-panel { animation: none; }
|
||||
.why-panel,
|
||||
.why-panel.is-active {
|
||||
transition: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@@ -1613,7 +1635,7 @@
|
||||
}
|
||||
|
||||
.why-tab.is-active.is-cycling .why-tab-progress {
|
||||
animation: whyTabProgress var(--why-cycle-ms, 7000ms) cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
animation: whyTabProgress var(--why-cycle-ms, 7000ms) linear forwards;
|
||||
}
|
||||
|
||||
@keyframes whyTabProgress {
|
||||
@@ -2182,12 +2204,50 @@
|
||||
/* ─ Panel 01: PRODUCT.md file view ─ */
|
||||
.why-visual--productmd {
|
||||
padding: 0;
|
||||
background: var(--color-paper);
|
||||
background: transparent;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--spacing-lg);
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 780px) {
|
||||
.why-visual--productmd {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.why-productmd-file {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-paper);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
.why-productmd-commands {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
min-width: 160px;
|
||||
}
|
||||
.why-productmd-commands-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9.5px;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-ash);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.why-productmd-commands code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--color-ink);
|
||||
background: var(--color-paper);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 5px;
|
||||
padding: 5px 9px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.why-productmd-header {
|
||||
display: flex;
|
||||
@@ -2701,590 +2761,6 @@
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LIVE MODE — interactive demo loop
|
||||
============================================ */
|
||||
|
||||
.live-section {
|
||||
padding: var(--spacing-2xl) 0;
|
||||
border-top: 1px solid var(--color-mist);
|
||||
}
|
||||
|
||||
.live-content .section-lead {
|
||||
max-width: 64ch;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.live-demo {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
|
||||
gap: var(--spacing-xl);
|
||||
align-items: start;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
.live-demo-frame-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
min-width: 0;
|
||||
}
|
||||
@media (max-width: 920px) {
|
||||
.live-demo { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.live-demo-frame {
|
||||
position: relative;
|
||||
background: var(--color-cream);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16 / 9;
|
||||
max-width: 960px;
|
||||
box-shadow: 0 20px 50px oklch(0% 0 0 / 0.08);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.live-demo-chrome {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--color-mist);
|
||||
background: var(--color-paper);
|
||||
}
|
||||
.live-demo-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-mist);
|
||||
}
|
||||
.live-demo-url {
|
||||
margin-left: 14px;
|
||||
padding: 4px 14px;
|
||||
background: var(--color-cream);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 5px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
|
||||
.live-demo-stage {
|
||||
position: relative;
|
||||
height: calc(100% - 42px - 60px); /* minus chrome + bar */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Target card */
|
||||
.live-demo-target {
|
||||
position: relative;
|
||||
width: min(360px, 80%);
|
||||
min-height: 200px;
|
||||
}
|
||||
.live-demo-variant {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.99);
|
||||
transition: opacity 280ms var(--ease-out), transform 280ms var(--ease-out);
|
||||
pointer-events: none;
|
||||
}
|
||||
.live-demo-variant.is-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
.live-demo-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 22px 24px;
|
||||
background: var(--color-paper);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.live-demo-card-kicker {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-ash);
|
||||
}
|
||||
.live-demo-card h3 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
color: var(--color-ink);
|
||||
margin: 0;
|
||||
}
|
||||
.live-demo-card p {
|
||||
font-family: var(--font-body);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-charcoal);
|
||||
margin: 0;
|
||||
}
|
||||
.live-demo-card button {
|
||||
align-self: flex-start;
|
||||
margin-top: auto;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
padding: 9px 16px;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Per-variant look */
|
||||
.live-demo-card--plain {}
|
||||
.live-demo-card--v1 {
|
||||
background: var(--color-cream);
|
||||
}
|
||||
.live-demo-card--v1 h3 { font-style: italic; }
|
||||
.live-demo-card--v1 h3 em { color: var(--color-accent); font-style: italic; }
|
||||
.live-demo-card--v2 {
|
||||
background: var(--color-paper);
|
||||
border: 1px dashed var(--color-accent);
|
||||
}
|
||||
.live-demo-card--v2 .live-demo-card-kicker { color: var(--color-accent); }
|
||||
.live-demo-card--v2 h3 { font-family: var(--font-display); font-style: italic; }
|
||||
.live-demo-card--v2 button {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
.live-demo-card--v3 {
|
||||
background: oklch(96% 0.02 350);
|
||||
border: 1px solid var(--color-accent-soft);
|
||||
}
|
||||
.live-demo-card--v3 h3 em { color: var(--color-accent); font-style: italic; }
|
||||
.live-demo-card--v3 .live-demo-card-kicker { color: var(--color-accent-hover); }
|
||||
|
||||
/* Highlight outline over the target */
|
||||
.live-demo-outline {
|
||||
position: absolute;
|
||||
border: 2px solid var(--color-accent);
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms var(--ease-out), top 320ms var(--ease-out), left 320ms var(--ease-out), width 320ms var(--ease-out), height 320ms var(--ease-out);
|
||||
box-shadow: 0 0 0 4px var(--color-accent-dim);
|
||||
}
|
||||
.live-demo-outline.is-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Annotations */
|
||||
.live-demo-annotations {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 280ms var(--ease-out);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.live-demo-annotations.is-visible { opacity: 1; }
|
||||
.live-demo-stroke {
|
||||
position: absolute;
|
||||
width: 280px;
|
||||
height: 56px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -24px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.live-demo-stroke path {
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
.live-demo-annotations.is-visible .live-demo-stroke path {
|
||||
animation: liveDemoStroke 800ms var(--ease-out) forwards;
|
||||
}
|
||||
@keyframes liveDemoStroke {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
.live-demo-comment {
|
||||
position: absolute;
|
||||
top: 56%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
padding: 5px 10px;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms var(--ease-out);
|
||||
}
|
||||
.live-demo-annotations.is-comment-visible .live-demo-comment { opacity: 1; }
|
||||
|
||||
/* Simulated cursor */
|
||||
.live-demo-cursor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 18px;
|
||||
height: 22px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: translate(0, 0);
|
||||
transition: opacity 200ms var(--ease-out), transform 560ms var(--ease-out-quint);
|
||||
z-index: 5;
|
||||
filter: drop-shadow(0 2px 4px oklch(0% 0 0 / 0.2));
|
||||
}
|
||||
.live-demo-cursor.is-visible { opacity: 1; }
|
||||
.live-demo-cursor.is-click svg path {
|
||||
transform-origin: 4px 4px;
|
||||
animation: liveDemoCursorClick 220ms var(--ease-out);
|
||||
}
|
||||
@keyframes liveDemoCursorClick {
|
||||
0%, 100% { transform: scale(1); }
|
||||
40% { transform: scale(0.78); }
|
||||
}
|
||||
|
||||
/* Global bar — the persistent dark pill at the bottom of every session. */
|
||||
.live-demo-gbar {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-height: 36px;
|
||||
padding: 0 4px;
|
||||
background: oklch(14% 0 0);
|
||||
color: oklch(92% 0 0);
|
||||
border: 1px solid oklch(22% 0 0);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
box-shadow: 0 8px 24px oklch(0% 0 0 / 0.2);
|
||||
z-index: 4;
|
||||
}
|
||||
.live-demo-gbar-brand {
|
||||
font-family: var(--font-display);
|
||||
font-size: 16px;
|
||||
color: var(--color-accent);
|
||||
padding: 0 10px;
|
||||
}
|
||||
.live-demo-gbar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 10px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
color: oklch(75% 0 0);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.live-demo-gbar-btn.is-active {
|
||||
background: var(--color-accent-dim);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.live-demo-gbar-dmd {
|
||||
display: inline-grid;
|
||||
grid-template: repeat(2, 1fr) / repeat(2, 1fr);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.live-demo-gbar-dmd span:nth-child(1) { background: oklch(60% 0.25 350); }
|
||||
.live-demo-gbar-dmd span:nth-child(2) { background: oklch(60% 0.15 45); }
|
||||
.live-demo-gbar-dmd span:nth-child(3) { background: oklch(55% 0.12 250); }
|
||||
.live-demo-gbar-dmd span:nth-child(4) { background: oklch(30% 0 0); }
|
||||
.live-demo-gbar-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: oklch(28% 0 0);
|
||||
margin: 0 4px;
|
||||
}
|
||||
.live-demo-gbar-x {
|
||||
padding: 7px 10px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
color: oklch(60% 0 0);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Contextual bar — LIGHT paper-backed pill that floats near the picked
|
||||
element during a session. Matches live-browser.js's buildConfigureRow
|
||||
look (dark command pill, transparent input, ×N count, magenta Go). */
|
||||
.live-demo-ctx {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 6px);
|
||||
padding: 6px;
|
||||
background: var(--color-paper);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06);
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
color: var(--color-ink);
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity 260ms var(--ease-out), transform 260ms var(--ease-out);
|
||||
z-index: 3;
|
||||
max-width: 90%;
|
||||
}
|
||||
.live-demo-ctx[data-phase="configuring"],
|
||||
.live-demo-ctx[data-phase="generating"],
|
||||
.live-demo-ctx[data-phase="cycling"],
|
||||
.live-demo-ctx[data-phase="accepted"] {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
.live-demo-ctx-row {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.live-demo-ctx[data-phase="configuring"] .live-demo-ctx-row--configure,
|
||||
.live-demo-ctx[data-phase="generating"] .live-demo-ctx-row--generating,
|
||||
.live-demo-ctx[data-phase="cycling"] .live-demo-ctx-row--cycling,
|
||||
.live-demo-ctx[data-phase="accepted"] .live-demo-ctx-row--accepted {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Configure row: [delight ▾] [input] [×3] [Go →] */
|
||||
.live-demo-ctx-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 10px;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.live-demo-ctx-pill-caret { font-size: 9px; opacity: 0.7; margin-left: 2px; }
|
||||
.live-demo-ctx-input {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
padding: 5px 8px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.live-demo-ctx-caret {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 13px;
|
||||
background: var(--color-ink);
|
||||
margin-left: 2px;
|
||||
animation: liveDemoCaret 1s steps(1) infinite;
|
||||
}
|
||||
@keyframes liveDemoCaret {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
.live-demo-ctx-count {
|
||||
padding: 4px 6px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 5px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--color-ash);
|
||||
cursor: pointer;
|
||||
}
|
||||
.live-demo-ctx-go {
|
||||
padding: 5px 12px;
|
||||
background: var(--color-accent);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Generating row */
|
||||
.live-demo-ctx-row--generating {
|
||||
gap: 10px;
|
||||
padding: 4px 12px 4px 6px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
.live-demo-ctx-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid var(--color-mist);
|
||||
border-top-color: var(--color-accent);
|
||||
animation: liveDemoSpin 700ms linear infinite;
|
||||
}
|
||||
@keyframes liveDemoSpin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Cycling row */
|
||||
.live-demo-ctx-row--cycling { gap: 2px; padding: 2px; }
|
||||
.live-demo-ctx-nav {
|
||||
padding: 4px 10px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
color: var(--color-charcoal);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.live-demo-ctx-counter {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--color-ink);
|
||||
padding: 0 6px;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
.live-demo-ctx-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: var(--color-mist);
|
||||
margin: 0 4px;
|
||||
}
|
||||
.live-demo-ctx-discard {
|
||||
padding: 4px 10px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
color: var(--color-ash);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.live-demo-ctx-accept {
|
||||
padding: 5px 14px;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Accepted confirmation row */
|
||||
.live-demo-ctx-row--accepted {
|
||||
gap: 8px;
|
||||
padding: 6px 14px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
color: oklch(45% 0.18 145);
|
||||
}
|
||||
|
||||
/* Phase caption under the frame */
|
||||
.live-demo-caption {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-ash);
|
||||
}
|
||||
.live-demo-caption::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent);
|
||||
animation: liveDemoPulse 1.6s var(--ease-out) infinite;
|
||||
}
|
||||
@keyframes liveDemoPulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(0.7); }
|
||||
}
|
||||
.live-demo-caption-label { color: var(--color-ink); }
|
||||
|
||||
/* Supporting row under the demo */
|
||||
.live-demo-support {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
align-self: stretch;
|
||||
}
|
||||
.live-demo-support-cell {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px dashed var(--color-mist);
|
||||
}
|
||||
.live-demo-support-cell:last-child {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.live-demo-support-k {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.live-demo-support-v {
|
||||
font-family: var(--font-body);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.live-demo-support-v code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875em;
|
||||
background: var(--color-mist);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.live-demo-frame { aspect-ratio: 4 / 5; }
|
||||
.live-demo-target { width: 90%; }
|
||||
.live-demo-bar { font-size: 11px; min-height: 32px; }
|
||||
.live-demo-support { grid-template-columns: 1fr; gap: var(--spacing-md); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.live-demo-cursor,
|
||||
.live-demo-outline,
|
||||
.live-demo-variant,
|
||||
.live-demo-annotations,
|
||||
.live-demo-stroke path {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
.live-demo-caption::before { animation: none; }
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LANGUAGE section — Palette / Periodic tabs
|
||||
@@ -3295,7 +2771,6 @@
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.language-intro-row .section-lead {
|
||||
@@ -3449,224 +2924,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LIVE MODE — skeleton page + richer variants
|
||||
============================================ */
|
||||
|
||||
/* Skeleton elements behind the target card — suggest "this is a real page" */
|
||||
.live-demo-skeleton {
|
||||
position: absolute;
|
||||
inset: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
filter: blur(0.3px);
|
||||
}
|
||||
.live-demo-skel-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--color-mist);
|
||||
}
|
||||
.live-demo-skel-logo {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
background: var(--color-charcoal);
|
||||
}
|
||||
.live-demo-skel-link {
|
||||
width: 48px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-mist);
|
||||
}
|
||||
.live-demo-skel-cta {
|
||||
margin-left: auto;
|
||||
width: 72px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background: var(--color-charcoal);
|
||||
}
|
||||
.live-demo-skel-heading {
|
||||
width: 60%;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
background: var(--color-mist);
|
||||
margin-top: 20px;
|
||||
}
|
||||
.live-demo-skel-line {
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-mist);
|
||||
}
|
||||
.live-demo-skel-line--short { width: 40%; }
|
||||
|
||||
/* Target stays above skeleton */
|
||||
.live-demo-target { position: relative; z-index: 1; }
|
||||
|
||||
/* ---- Card variants — sharper, more distinct ---- */
|
||||
|
||||
/* Variant 1: magazine column — italic serif, rule lines, off-white */
|
||||
.live-demo-card--v1 {
|
||||
background: var(--color-cream);
|
||||
padding: 18px 22px;
|
||||
border: 0;
|
||||
border-top: 3px solid var(--color-ink);
|
||||
border-radius: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
.live-demo-card--v1 .live-demo-card-kicker {
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.28em;
|
||||
}
|
||||
.live-demo-card--v1 h3 {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 28px;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.01em;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.live-demo-card--v1 h3 em { color: var(--color-accent); font-style: italic; }
|
||||
.live-demo-card--v1 p {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
.live-demo-card--v1 button {
|
||||
font-family: var(--font-body);
|
||||
align-self: flex-start;
|
||||
margin-top: 6px;
|
||||
background: transparent;
|
||||
color: var(--color-ink);
|
||||
border: 0;
|
||||
border-bottom: 1.5px solid var(--color-ink);
|
||||
padding: 4px 0;
|
||||
border-radius: 0;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
/* Variant 2: brutalist ticket — thick ink block, stamp glyph, mono */
|
||||
.live-demo-card--v2 {
|
||||
position: relative;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
padding: 20px 24px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
gap: 8px;
|
||||
}
|
||||
.live-demo-card--v2::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
background: var(--color-accent);
|
||||
}
|
||||
.live-demo-card-stamp {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 18px;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
color: var(--color-accent);
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
.live-demo-card--v2 .live-demo-card-kicker {
|
||||
color: var(--color-accent);
|
||||
letter-spacing: 0.3em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.live-demo-card--v2 h3 {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
line-height: 1.1;
|
||||
color: var(--color-paper);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.live-demo-card--v2 button {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
align-self: flex-start;
|
||||
margin-top: 6px;
|
||||
background: var(--color-accent);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 9px 14px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Variant 3: playful ticket — accent background, sticker stars, postcard vibe */
|
||||
.live-demo-card--v3 {
|
||||
position: relative;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, oklch(92% 0.08 350) 0, transparent 45%),
|
||||
var(--color-cream);
|
||||
border: 1px dashed var(--color-accent);
|
||||
border-radius: 10px;
|
||||
padding: 22px 24px 20px;
|
||||
gap: 8px;
|
||||
}
|
||||
.live-demo-card-sticker {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 14px;
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
color: var(--color-accent);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
transform: rotate(6deg);
|
||||
}
|
||||
.live-demo-card--v3 .live-demo-card-kicker {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-accent-hover);
|
||||
text-transform: none;
|
||||
}
|
||||
.live-demo-card--v3 h3 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 400;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.live-demo-card--v3 button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
align-self: flex-start;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 10px 18px;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: none;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ANTIDOTE — visual-first wall of three
|
||||
============================================ */
|
||||
@@ -4014,6 +3271,21 @@
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.gallery-stack-link {
|
||||
display: inline-flex;
|
||||
align-self: center;
|
||||
margin-top: var(--spacing-sm);
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-ink);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
text-decoration-color: var(--color-accent);
|
||||
transition: color 0.12s ease;
|
||||
}
|
||||
|
||||
.gallery-stack-link:hover { color: var(--color-accent); }
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.why-visual--antidote { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
+13
-14
@@ -60,7 +60,7 @@
|
||||
<a href="/" data-nav="home" aria-current="page">Home</a>
|
||||
<a href="/docs" data-nav="docs">Docs</a>
|
||||
<a href="/anti-patterns" data-nav="anti-patterns">Anti-Patterns</a>
|
||||
<a href="/docs/live" data-nav="live">Live</a>
|
||||
<a href="/live-mode" data-nav="live">Live</a>
|
||||
<a href="/visual-mode" data-nav="visual-mode">Overlay</a>
|
||||
</nav>
|
||||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
|
||||
@@ -288,13 +288,14 @@
|
||||
<span class="why-productmd-v">Purple gradients. Glassmorphism. "Boost your productivity."</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="why-productmd-footer">
|
||||
<span class="why-productmd-footer-dot"></span>
|
||||
Every command reads this before writing a line of code.
|
||||
</div>
|
||||
</div>
|
||||
<aside class="why-productmd-commands">
|
||||
<span class="why-productmd-commands-label">Commands</span>
|
||||
<code>/impeccable teach</code>
|
||||
<code>/impeccable document</code>
|
||||
<code>/impeccable shape</code>
|
||||
</aside>
|
||||
</div>
|
||||
<p class="why-panel-meta"><code>/impeccable teach</code> · <code>/impeccable document</code> · <code>/impeccable shape</code></p>
|
||||
</article>
|
||||
|
||||
<article class="why-panel why-panel--antidote" id="why-panel-1" role="tabpanel" data-index="1" hidden>
|
||||
@@ -336,9 +337,9 @@
|
||||
<span class="gallery-stack-label">Bad Contrast</span>
|
||||
</a>
|
||||
</div>
|
||||
<a class="gallery-stack-link" href="/anti-patterns">Browse the full catalog →</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="why-panel-meta"><a href="/anti-patterns">Browse the full catalog →</a></p>
|
||||
</article>
|
||||
|
||||
<article class="why-panel" id="why-panel-2" role="tabpanel" data-index="2" hidden>
|
||||
@@ -370,17 +371,17 @@
|
||||
|
||||
<article class="why-panel" id="why-panel-3" role="tabpanel" data-index="3" hidden>
|
||||
<h3 class="why-panel-title">Brand work is not product UI.</h3>
|
||||
<p class="why-panel-body">Most design skills, including Anthropic's own frontend-design skill, collapse everything into one vocabulary. But a landing page and a dashboard live by contradictory rules. Impeccable carries two registers: <em>brand</em> (design IS the product: marketing, portfolios, editorial) and <em>product</em> (design SERVES the product: app UI, tools, dashboards). Register-aware commands (<code>typeset</code>, <code>animate</code>, <code>colorize</code>, <code>bolder</code>, <code>quieter</code>, <code>layout</code>, <code>delight</code>) adjust their choices to match.</p>
|
||||
<p class="why-panel-body">Most design skills, including Anthropic's own frontend-design skill, collapse everything into one vocabulary. But a landing page and a dashboard live by contradictory rules. Impeccable runs in two modes: <em>brand</em> (design IS the product: marketing, portfolios, editorial) and <em>product</em> (design SERVES the product: app UI, tools, dashboards). Commands like <code>typeset</code>, <code>animate</code>, <code>colorize</code>, <code>bolder</code>, <code>quieter</code>, <code>layout</code>, and <code>delight</code> adjust their choices to match.</p>
|
||||
<div class="why-visual why-visual--registers">
|
||||
<div class="why-register why-register--brand">
|
||||
<span class="why-register-label">Brand register</span>
|
||||
<span class="why-register-label">Brand mode</span>
|
||||
<div class="why-register-mock why-register-mock--brand">
|
||||
<span class="why-brand-hero-mono">Issue 04</span>
|
||||
<div class="why-brand-hero-title"><em>Shape</em> the story.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="why-register why-register--product">
|
||||
<span class="why-register-label">Product register</span>
|
||||
<span class="why-register-label">Product mode</span>
|
||||
<div class="why-register-mock why-register-mock--product">
|
||||
<div class="why-product-row"><span class="why-product-k">Users</span><span class="why-product-v">12,482</span></div>
|
||||
<div class="why-product-row"><span class="why-product-k">Active now</span><span class="why-product-v">1,207</span></div>
|
||||
@@ -388,7 +389,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="why-panel-meta">The register is inferred from your task and confirmed in <code>PRODUCT.md</code>.</p>
|
||||
<p class="why-panel-meta">The mode is inferred from your task and confirmed in <code>PRODUCT.md</code>.</p>
|
||||
</article>
|
||||
|
||||
<article class="why-panel" id="why-panel-4" role="tabpanel" data-index="4" hidden>
|
||||
@@ -424,7 +425,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="why-panel-meta">Works in Claude Code, Cursor, Codex, Gemini CLI, Antigravity, Kiro, OpenCode, and more.</p>
|
||||
</article>
|
||||
|
||||
<article class="why-panel" id="why-panel-5" role="tabpanel" data-index="5" hidden>
|
||||
@@ -471,7 +471,6 @@
|
||||
</div>
|
||||
<div class="why-dm-foot">Readable by every DESIGN.md-aware tool.</div>
|
||||
</div>
|
||||
<p class="why-panel-meta">Spec-compliant. Interoperable. Not a proprietary sidecar.</p>
|
||||
</article>
|
||||
|
||||
<article class="why-panel" id="why-panel-6" role="tabpanel" data-index="6" hidden>
|
||||
@@ -1143,7 +1142,7 @@
|
||||
<nav class="footer-links" aria-label="Footer">
|
||||
<a href="/docs">Docs</a>
|
||||
<a href="/anti-patterns">Anti-Patterns</a>
|
||||
<a href="/docs/live">Live Mode</a>
|
||||
<a href="/live-mode">Live Mode</a>
|
||||
<a href="/visual-mode">Overlay</a>
|
||||
<a href="#language">Commands</a>
|
||||
<a href="/privacy">Privacy</a>
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Live Mode | Impeccable</title>
|
||||
<meta name="description" content="Iterate on UI in the browser. Pick an element, drop a comment, get three production-quality variants, accept one, and it writes back to source. /impeccable live.">
|
||||
<meta name="theme-color" content="#fafafa">
|
||||
<link rel="canonical" href="https://impeccable.style/live-mode">
|
||||
<link rel="icon" type="image/svg+xml" href="../favicon.svg">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../css/sub-pages.css">
|
||||
|
||||
<link rel="stylesheet" href="../css/live-mode.css">
|
||||
<script type="module">
|
||||
import { initLiveDemo } from "../js/components/live-demo.js";
|
||||
document.addEventListener("DOMContentLoaded", initLiveDemo);
|
||||
</script>
|
||||
</head>
|
||||
<body class="sub-page live-mode-page-body">
|
||||
<a href="#main" class="skip-link">Skip to content</a>
|
||||
<!-- site-header v1 -->
|
||||
<header class="site-header" data-site-header>
|
||||
<a href="/" class="site-header-brand" aria-label="Impeccable home">
|
||||
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
|
||||
<span class="site-header-brand-name">Impeccable</span>
|
||||
</a>
|
||||
<div class="site-header-right">
|
||||
<nav class="site-header-nav" aria-label="Primary">
|
||||
<a href="/" data-nav="home">Home</a>
|
||||
<a href="/docs" data-nav="docs">Docs</a>
|
||||
<a href="/anti-patterns" data-nav="anti-patterns">Anti-Patterns</a>
|
||||
<a href="/live-mode" data-nav="live" aria-current="page">Live</a>
|
||||
<a href="/visual-mode" data-nav="visual-mode">Overlay</a>
|
||||
</nav>
|
||||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
||||
<span class="site-header-github-label">21k</span>
|
||||
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<main id="main">
|
||||
|
||||
<div class="live-mode-page">
|
||||
<header class="live-mode-page-header">
|
||||
<p class="live-mode-page-eyebrow">New in v3.0 <span class="live-mode-page-eyebrow-badge">Beta</span></p>
|
||||
<h1 class="live-mode-page-title">Live Mode</h1>
|
||||
<p class="live-mode-page-lede">Pick any element in the browser. Drop a comment or a stroke. Three production-quality variants swap in via your framework's HMR. Accept the one you want and it writes back to source.</p>
|
||||
<div class="live-mode-start" aria-label="Start live mode command">
|
||||
<span class="live-mode-start-prompt">$</span>
|
||||
<code class="live-mode-start-cmd">/impeccable live</code>
|
||||
<button class="live-mode-start-copy" type="button" aria-label="Copy command" data-copy="/impeccable live">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="live-mode-demo-wrap" aria-label="Live Mode interactive demo">
|
||||
|
||||
<div class="live-demo" id="live-demo" aria-label="Live Mode interactive demo loop">
|
||||
<div class="live-demo-frame-col"><div class="live-demo-frame">
|
||||
<div class="live-demo-chrome">
|
||||
<span class="live-demo-dot"></span>
|
||||
<span class="live-demo-dot"></span>
|
||||
<span class="live-demo-dot"></span>
|
||||
<span class="live-demo-url">localhost:3000</span>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-stage">
|
||||
<div class="live-demo-skeleton" aria-hidden="true">
|
||||
<div class="live-demo-skel-nav">
|
||||
<span class="live-demo-skel-logo"></span>
|
||||
<span class="live-demo-skel-link"></span>
|
||||
<span class="live-demo-skel-link"></span>
|
||||
<span class="live-demo-skel-link"></span>
|
||||
<span class="live-demo-skel-cta"></span>
|
||||
</div>
|
||||
<div class="live-demo-skel-heading"></div>
|
||||
<div class="live-demo-skel-line"></div>
|
||||
<div class="live-demo-skel-line live-demo-skel-line--short"></div>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-target" data-demo-target>
|
||||
<div class="live-demo-variant is-active" data-variant="original">
|
||||
<div class="live-demo-card live-demo-card--plain">
|
||||
<span class="live-demo-card-kicker">Newsletter</span>
|
||||
<h3>Subscribe for updates</h3>
|
||||
<p>Monthly-ish design notes.</p>
|
||||
<button type="button">Subscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-demo-variant" data-variant="1">
|
||||
<div class="live-demo-card live-demo-card--v1">
|
||||
<span class="live-demo-card-kicker">No. 04</span>
|
||||
<h3>Letters, <em>occasionally</em>.</h3>
|
||||
<p>A postcard from the editor, about once a month. No tracking pixels, no "just checking in."</p>
|
||||
<button type="button">Send me one</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-demo-variant" data-variant="2">
|
||||
<div class="live-demo-card live-demo-card--v2">
|
||||
<div class="live-demo-card-stamp">☞</div>
|
||||
<span class="live-demo-card-kicker">Dispatch</span>
|
||||
<h3>Design notes, <br>every other<br>Thursday.</h3>
|
||||
<button type="button">Join the list →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-demo-variant" data-variant="3">
|
||||
<div class="live-demo-card live-demo-card--v3">
|
||||
<div class="live-demo-card-sticker"><span>☆</span><span>☆</span><span>☆</span></div>
|
||||
<span class="live-demo-card-kicker">Field Notes</span>
|
||||
<h3>A monthly letter, for people who still read email for pleasure.</h3>
|
||||
<button type="button">Receive the letter <span aria-hidden="true">✺</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-outline" data-demo-outline aria-hidden="true"></div>
|
||||
|
||||
<div class="live-demo-annotations" data-demo-annotations aria-hidden="true">
|
||||
<svg class="live-demo-stroke" viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path d="M 10,40 Q 60,10 110,38 T 210,32 T 290,20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" pathLength="1"/>
|
||||
</svg>
|
||||
<div class="live-demo-comment">more playful</div>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-ctx" data-demo-ctx data-phase="hidden">
|
||||
<div class="live-demo-ctx-row live-demo-ctx-row--configure">
|
||||
<button type="button" class="live-demo-ctx-pill" data-demo-ctx-pill>
|
||||
<span data-demo-cmd-name>delight</span>
|
||||
<span class="live-demo-ctx-pill-caret" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<span class="live-demo-ctx-input" data-demo-input>
|
||||
<span data-demo-input-text></span><span class="live-demo-ctx-caret"></span>
|
||||
</span>
|
||||
<button type="button" class="live-demo-ctx-count">×3</button>
|
||||
<button type="button" class="live-demo-ctx-go" data-demo-go>Go <span aria-hidden="true">→</span></button>
|
||||
</div>
|
||||
<div class="live-demo-ctx-row live-demo-ctx-row--generating">
|
||||
<span class="live-demo-ctx-spinner" aria-hidden="true"></span>
|
||||
<span>Generating variants…</span>
|
||||
</div>
|
||||
<div class="live-demo-ctx-row live-demo-ctx-row--cycling">
|
||||
<button type="button" class="live-demo-ctx-nav" aria-label="Previous variant">‹</button>
|
||||
<span class="live-demo-ctx-counter" data-demo-counter>1 / 3</span>
|
||||
<button type="button" class="live-demo-ctx-nav" aria-label="Next variant">›</button>
|
||||
<span class="live-demo-ctx-divider"></span>
|
||||
<button type="button" class="live-demo-ctx-discard" aria-label="Discard">✕</button>
|
||||
<button type="button" class="live-demo-ctx-accept" data-demo-accept>Accept</button>
|
||||
</div>
|
||||
<div class="live-demo-ctx-row live-demo-ctx-row--accepted">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
<span>Variant 3 written to source</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-cursor" data-demo-cursor aria-hidden="true">
|
||||
<svg width="18" height="22" viewBox="0 0 18 22" fill="none">
|
||||
<path d="M1 1 L1 17 L5 13 L8 20 L11 19 L7.5 12 L13 12 Z" fill="#111" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-gbar" data-demo-gbar>
|
||||
<span class="live-demo-gbar-brand">/</span>
|
||||
<button type="button" class="live-demo-gbar-btn is-active" data-demo-gbar-pick>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>
|
||||
<span>Pick</span>
|
||||
</button>
|
||||
<button type="button" class="live-demo-gbar-btn">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
<button type="button" class="live-demo-gbar-btn">
|
||||
<span class="live-demo-gbar-dmd" aria-hidden="true"><span></span><span></span><span></span><span></span></span>
|
||||
</button>
|
||||
<span class="live-demo-gbar-divider"></span>
|
||||
<button type="button" class="live-demo-gbar-x" aria-label="Exit live mode">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="live-mode-demo-caption">Click the frame or scroll it into view to start the loop. Respects <code>prefers-reduced-motion</code>.</p>
|
||||
</section>
|
||||
|
||||
<section class="live-mode-stages" aria-label="What happens in a live mode session">
|
||||
<h2 class="live-mode-stages-title">What happens, in three moves</h2>
|
||||
<div class="live-mode-stages-grid">
|
||||
<article class="live-mode-stage">
|
||||
<span class="live-mode-stage-num">01 · Pick</span>
|
||||
<h3 class="live-mode-stage-name">Point at what bugs you</h3>
|
||||
<p class="live-mode-stage-desc">Click any element on your running dev server. Add a comment pin where the issue lives. Draw a stroke through the bit you want to change. Or just type "more playful".</p>
|
||||
<div class="live-mode-stage-viz">
|
||||
<div class="docs-viz-picker-row" style="min-height:72px;padding:10px">
|
||||
<div class="docs-viz-picker-target" style="font-size:12px;padding:6px 12px">
|
||||
Newsletter card
|
||||
<span class="docs-viz-picker-pin" style="width:18px;height:18px;font-size:9px">1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="live-mode-stage">
|
||||
<span class="live-mode-stage-num">02 · Generate</span>
|
||||
<h3 class="live-mode-stage-name">Three genuinely different takes</h3>
|
||||
<p class="live-mode-stage-desc">Variants anchor to different archetypes, not three riffs on color. Each one explores a different primary axis: hierarchy, typography, density, layout, or palette strategy.</p>
|
||||
<div class="live-mode-stage-viz">
|
||||
<div class="docs-viz-variants" style="width:100%;gap:4px">
|
||||
<div class="docs-viz-variant docs-viz-variant--v1" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">No.04</span></div>
|
||||
<div class="docs-viz-variant docs-viz-variant--v2 is-active" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Dispatch</span></div>
|
||||
<div class="docs-viz-variant docs-viz-variant--v3" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Field</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="live-mode-stage">
|
||||
<span class="live-mode-stage-num">03 · Accept</span>
|
||||
<h3 class="live-mode-stage-name">Lands in real source</h3>
|
||||
<p class="live-mode-stage-desc">The accepted variant replaces the picked element in your source file. CSS consolidates into your real stylesheet, not inline. Discard all three and the original stays.</p>
|
||||
<div class="live-mode-stage-viz">
|
||||
<span class="docs-viz-accept-pill">Variant 2 written to source</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-mode-pathways" aria-label="Where to go next">
|
||||
<h2 class="live-mode-pathways-title">Where next</h2>
|
||||
<div class="live-mode-pathways-grid">
|
||||
<a class="live-mode-pathway" href="/tutorials/iterate-live">
|
||||
<span class="live-mode-pathway-kind">Tutorial</span>
|
||||
<h3 class="live-mode-pathway-title">Walk it step by step</h3>
|
||||
<p class="live-mode-pathway-desc">A ten-minute walkthrough from first run to accepted variant. Covers CSP patching, the picker actions, and the fallback flow for generated files.</p>
|
||||
<span class="live-mode-pathway-cta">Open the tutorial →</span>
|
||||
</a>
|
||||
<a class="live-mode-pathway" href="/docs/live">
|
||||
<span class="live-mode-pathway-kind">Reference</span>
|
||||
<h3 class="live-mode-pathway-title">Full command reference</h3>
|
||||
<p class="live-mode-pathway-desc">Everything your AI harness reads when <code>/impeccable live</code> runs: the poll loop, the wrap/accept helpers, the CSP templates, and every event shape.</p>
|
||||
<span class="live-mode-pathway-cta">Read the reference →</span>
|
||||
</a>
|
||||
<a class="live-mode-pathway" href="/#downloads">
|
||||
<span class="live-mode-pathway-kind">Install</span>
|
||||
<h3 class="live-mode-pathway-title">Get Impeccable set up</h3>
|
||||
<p class="live-mode-pathway-desc">Install the skill and CLI once, then run <code>/impeccable live</code> from your AI harness. Works with Claude Code, Cursor, Codex, Gemini, and more.</p>
|
||||
<span class="live-mode-pathway-cta">See the install steps →</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-mode-frameworks" aria-label="Supported frameworks">
|
||||
<span class="live-mode-frameworks-label">Supported dev servers</span>
|
||||
<ul class="live-mode-frameworks-list">
|
||||
<li>Vite</li>
|
||||
<li>Next.js (incl. monorepos)</li>
|
||||
<li>SvelteKit</li>
|
||||
<li>Astro</li>
|
||||
<li>Nuxt</li>
|
||||
<li>Bun</li>
|
||||
<li>Plain static HTML</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
// Copy buttons on rendered code blocks
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-copy]');
|
||||
if (!btn) return;
|
||||
const text = btn.getAttribute('data-copy');
|
||||
if (!text) return;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
btn.classList.add('is-copied');
|
||||
setTimeout(() => btn.classList.remove('is-copied'), 1500);
|
||||
}).catch(() => {});
|
||||
});
|
||||
|
||||
// Mobile sidebar toggle (shown on narrow viewports, hidden on desktop).
|
||||
document.addEventListener('click', (e) => {
|
||||
const toggle = e.target.closest('.skills-sidebar-toggle');
|
||||
if (!toggle) return;
|
||||
const expanded = toggle.getAttribute('aria-expanded') === 'true';
|
||||
toggle.setAttribute('aria-expanded', String(!expanded));
|
||||
});
|
||||
|
||||
// Before/after split-compare: drag on touch, hover OR drag on mouse.
|
||||
// Pointer events attach to the padded .split-comparison wrapper so
|
||||
// there is a ~20px invisible buffer around the visible box. The
|
||||
// divider only snaps back when the pointer leaves that outer buffer.
|
||||
(function initSplitCompare() {
|
||||
const wrappers = document.querySelectorAll('.split-comparison');
|
||||
if (wrappers.length === 0) return;
|
||||
const hasHover = matchMedia('(hover: hover)').matches;
|
||||
const DEFAULT_POSITION = 50;
|
||||
|
||||
for (const wrapper of wrappers) {
|
||||
const container = wrapper.querySelector('.split-container');
|
||||
const splitAfter = wrapper.querySelector('.split-after');
|
||||
const splitDivider = wrapper.querySelector('.split-divider');
|
||||
if (!container || !splitAfter || !splitDivider) continue;
|
||||
|
||||
const tanAngle = Math.tan(10 * Math.PI / 180);
|
||||
let skewOffset = 8;
|
||||
const recalcSkew = () => {
|
||||
const r = container.getBoundingClientRect();
|
||||
if (r.width > 0 && r.height > 0) {
|
||||
skewOffset = 50 * r.height * tanAngle / r.width;
|
||||
}
|
||||
};
|
||||
recalcSkew();
|
||||
window.addEventListener('resize', recalcSkew, { passive: true });
|
||||
|
||||
let targetX = DEFAULT_POSITION;
|
||||
let currentX = DEFAULT_POSITION;
|
||||
let rafId = null;
|
||||
|
||||
const paint = (pct) => {
|
||||
const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct));
|
||||
splitAfter.style.clipPath =
|
||||
`polygon(${x + skewOffset}% 0%, 100% 0%, 100% 100%, ${x - skewOffset}% 100%)`;
|
||||
splitDivider.style.left = `${x}%`;
|
||||
};
|
||||
|
||||
const step = () => {
|
||||
currentX += (targetX - currentX) * 0.2;
|
||||
if (Math.abs(targetX - currentX) < 0.1) {
|
||||
currentX = targetX;
|
||||
rafId = null;
|
||||
} else {
|
||||
rafId = requestAnimationFrame(step);
|
||||
}
|
||||
paint(currentX);
|
||||
};
|
||||
|
||||
const setTarget = (pct) => {
|
||||
targetX = pct;
|
||||
if (rafId === null) rafId = requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
paint(DEFAULT_POSITION);
|
||||
|
||||
// Percentage is always relative to the VISIBLE .split-container,
|
||||
// not the padded .split-comparison wrapper. The pointer event
|
||||
// target is the wrapper but the clip-path math uses the inner box.
|
||||
const pctFromClientX = (clientX) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
return ((clientX - rect.left) / rect.width) * 100;
|
||||
};
|
||||
|
||||
let hovering = false;
|
||||
let dragging = false;
|
||||
|
||||
wrapper.addEventListener('pointerenter', (e) => {
|
||||
if (hasHover && e.pointerType === 'mouse') {
|
||||
hovering = true;
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.addEventListener('pointerdown', (e) => {
|
||||
dragging = true;
|
||||
wrapper.setPointerCapture(e.pointerId);
|
||||
setTarget(pctFromClientX(e.clientX));
|
||||
});
|
||||
|
||||
wrapper.addEventListener('pointermove', (e) => {
|
||||
if (dragging || hovering) {
|
||||
setTarget(pctFromClientX(e.clientX));
|
||||
}
|
||||
});
|
||||
|
||||
const endDrag = (e) => {
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
try { wrapper.releasePointerCapture(e.pointerId); } catch {}
|
||||
}
|
||||
};
|
||||
|
||||
wrapper.addEventListener('pointerup', endDrag);
|
||||
wrapper.addEventListener('pointercancel', endDrag);
|
||||
|
||||
wrapper.addEventListener('pointerleave', (e) => {
|
||||
endDrag(e);
|
||||
if (hovering) {
|
||||
hovering = false;
|
||||
setTarget(DEFAULT_POSITION);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+270
-1
@@ -597,6 +597,249 @@ ${specimenCards}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the animated Live Mode demo block. HTML structure matches the
|
||||
* homepage #live-demo exactly so public/js/components/live-demo.js can
|
||||
* drive it without modification. The CSS (.live-demo-*) lives in
|
||||
* public/css/live-mode.css, imported by main.css and also loaded on this
|
||||
* page via extraHead.
|
||||
*/
|
||||
function renderLiveModeDemo() {
|
||||
return `
|
||||
<div class="live-demo" id="live-demo" aria-label="Live Mode interactive demo loop">
|
||||
<div class="live-demo-frame-col"><div class="live-demo-frame">
|
||||
<div class="live-demo-chrome">
|
||||
<span class="live-demo-dot"></span>
|
||||
<span class="live-demo-dot"></span>
|
||||
<span class="live-demo-dot"></span>
|
||||
<span class="live-demo-url">localhost:3000</span>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-stage">
|
||||
<div class="live-demo-skeleton" aria-hidden="true">
|
||||
<div class="live-demo-skel-nav">
|
||||
<span class="live-demo-skel-logo"></span>
|
||||
<span class="live-demo-skel-link"></span>
|
||||
<span class="live-demo-skel-link"></span>
|
||||
<span class="live-demo-skel-link"></span>
|
||||
<span class="live-demo-skel-cta"></span>
|
||||
</div>
|
||||
<div class="live-demo-skel-heading"></div>
|
||||
<div class="live-demo-skel-line"></div>
|
||||
<div class="live-demo-skel-line live-demo-skel-line--short"></div>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-target" data-demo-target>
|
||||
<div class="live-demo-variant is-active" data-variant="original">
|
||||
<div class="live-demo-card live-demo-card--plain">
|
||||
<span class="live-demo-card-kicker">Newsletter</span>
|
||||
<h3>Subscribe for updates</h3>
|
||||
<p>Monthly-ish design notes.</p>
|
||||
<button type="button">Subscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-demo-variant" data-variant="1">
|
||||
<div class="live-demo-card live-demo-card--v1">
|
||||
<span class="live-demo-card-kicker">No. 04</span>
|
||||
<h3>Letters, <em>occasionally</em>.</h3>
|
||||
<p>A postcard from the editor, about once a month. No tracking pixels, no "just checking in."</p>
|
||||
<button type="button">Send me one</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-demo-variant" data-variant="2">
|
||||
<div class="live-demo-card live-demo-card--v2">
|
||||
<div class="live-demo-card-stamp">☞</div>
|
||||
<span class="live-demo-card-kicker">Dispatch</span>
|
||||
<h3>Design notes, <br>every other<br>Thursday.</h3>
|
||||
<button type="button">Join the list →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-demo-variant" data-variant="3">
|
||||
<div class="live-demo-card live-demo-card--v3">
|
||||
<div class="live-demo-card-sticker"><span>☆</span><span>☆</span><span>☆</span></div>
|
||||
<span class="live-demo-card-kicker">Field Notes</span>
|
||||
<h3>A monthly letter, for people who still read email for pleasure.</h3>
|
||||
<button type="button">Receive the letter <span aria-hidden="true">✺</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-outline" data-demo-outline aria-hidden="true"></div>
|
||||
|
||||
<div class="live-demo-annotations" data-demo-annotations aria-hidden="true">
|
||||
<svg class="live-demo-stroke" viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path d="M 10,40 Q 60,10 110,38 T 210,32 T 290,20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" pathLength="1"/>
|
||||
</svg>
|
||||
<div class="live-demo-comment">more playful</div>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-ctx" data-demo-ctx data-phase="hidden">
|
||||
<div class="live-demo-ctx-row live-demo-ctx-row--configure">
|
||||
<button type="button" class="live-demo-ctx-pill" data-demo-ctx-pill>
|
||||
<span data-demo-cmd-name>delight</span>
|
||||
<span class="live-demo-ctx-pill-caret" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<span class="live-demo-ctx-input" data-demo-input>
|
||||
<span data-demo-input-text></span><span class="live-demo-ctx-caret"></span>
|
||||
</span>
|
||||
<button type="button" class="live-demo-ctx-count">×3</button>
|
||||
<button type="button" class="live-demo-ctx-go" data-demo-go>Go <span aria-hidden="true">→</span></button>
|
||||
</div>
|
||||
<div class="live-demo-ctx-row live-demo-ctx-row--generating">
|
||||
<span class="live-demo-ctx-spinner" aria-hidden="true"></span>
|
||||
<span>Generating variants…</span>
|
||||
</div>
|
||||
<div class="live-demo-ctx-row live-demo-ctx-row--cycling">
|
||||
<button type="button" class="live-demo-ctx-nav" aria-label="Previous variant">‹</button>
|
||||
<span class="live-demo-ctx-counter" data-demo-counter>1 / 3</span>
|
||||
<button type="button" class="live-demo-ctx-nav" aria-label="Next variant">›</button>
|
||||
<span class="live-demo-ctx-divider"></span>
|
||||
<button type="button" class="live-demo-ctx-discard" aria-label="Discard">✕</button>
|
||||
<button type="button" class="live-demo-ctx-accept" data-demo-accept>Accept</button>
|
||||
</div>
|
||||
<div class="live-demo-ctx-row live-demo-ctx-row--accepted">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
<span>Variant 3 written to source</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-cursor" data-demo-cursor aria-hidden="true">
|
||||
<svg width="18" height="22" viewBox="0 0 18 22" fill="none">
|
||||
<path d="M1 1 L1 17 L5 13 L8 20 L11 19 L7.5 12 L13 12 Z" fill="#111" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="live-demo-gbar" data-demo-gbar>
|
||||
<span class="live-demo-gbar-brand">/</span>
|
||||
<button type="button" class="live-demo-gbar-btn is-active" data-demo-gbar-pick>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>
|
||||
<span>Pick</span>
|
||||
</button>
|
||||
<button type="button" class="live-demo-gbar-btn">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
<button type="button" class="live-demo-gbar-btn">
|
||||
<span class="live-demo-gbar-dmd" aria-hidden="true"><span></span><span></span><span></span><span></span></span>
|
||||
</button>
|
||||
<span class="live-demo-gbar-divider"></span>
|
||||
<button type="button" class="live-demo-gbar-x" aria-label="Exit live mode">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the /live-mode page main content.
|
||||
*
|
||||
* Marketing-style single-column layout mirroring /visual-mode's structure.
|
||||
* Surfaces the animated homepage live-demo, a three-stage narrative,
|
||||
* pathway cards (tutorial / reference / install), and the supported
|
||||
* framework strip.
|
||||
*/
|
||||
function renderLiveModeMain() {
|
||||
return `
|
||||
<div class="live-mode-page">
|
||||
<header class="live-mode-page-header">
|
||||
<p class="live-mode-page-eyebrow">New in v3.0 <span class="live-mode-page-eyebrow-badge">Beta</span></p>
|
||||
<h1 class="live-mode-page-title">Live Mode</h1>
|
||||
<p class="live-mode-page-lede">Pick any element in the browser. Drop a comment or a stroke. Three production-quality variants swap in via your framework's HMR. Accept the one you want and it writes back to source.</p>
|
||||
<div class="live-mode-start" aria-label="Start live mode command">
|
||||
<span class="live-mode-start-prompt">$</span>
|
||||
<code class="live-mode-start-cmd">/impeccable live</code>
|
||||
<button class="live-mode-start-copy" type="button" aria-label="Copy command" data-copy="/impeccable live">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="live-mode-demo-wrap" aria-label="Live Mode interactive demo">
|
||||
${renderLiveModeDemo()}
|
||||
<p class="live-mode-demo-caption">Click the frame or scroll it into view to start the loop. Respects <code>prefers-reduced-motion</code>.</p>
|
||||
</section>
|
||||
|
||||
<section class="live-mode-stages" aria-label="What happens in a live mode session">
|
||||
<h2 class="live-mode-stages-title">What happens, in three moves</h2>
|
||||
<div class="live-mode-stages-grid">
|
||||
<article class="live-mode-stage">
|
||||
<span class="live-mode-stage-num">01 · Pick</span>
|
||||
<h3 class="live-mode-stage-name">Point at what bugs you</h3>
|
||||
<p class="live-mode-stage-desc">Click any element on your running dev server. Add a comment pin where the issue lives. Draw a stroke through the bit you want to change. Or just type "more playful".</p>
|
||||
<div class="live-mode-stage-viz">
|
||||
<div class="docs-viz-picker-row" style="min-height:72px;padding:10px">
|
||||
<div class="docs-viz-picker-target" style="font-size:12px;padding:6px 12px">
|
||||
Newsletter card
|
||||
<span class="docs-viz-picker-pin" style="width:18px;height:18px;font-size:9px">1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="live-mode-stage">
|
||||
<span class="live-mode-stage-num">02 · Generate</span>
|
||||
<h3 class="live-mode-stage-name">Three genuinely different takes</h3>
|
||||
<p class="live-mode-stage-desc">Variants anchor to different archetypes, not three riffs on color. Each one explores a different primary axis: hierarchy, typography, density, layout, or palette strategy.</p>
|
||||
<div class="live-mode-stage-viz">
|
||||
<div class="docs-viz-variants" style="width:100%;gap:4px">
|
||||
<div class="docs-viz-variant docs-viz-variant--v1" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">No.04</span></div>
|
||||
<div class="docs-viz-variant docs-viz-variant--v2 is-active" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Dispatch</span></div>
|
||||
<div class="docs-viz-variant docs-viz-variant--v3" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Field</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="live-mode-stage">
|
||||
<span class="live-mode-stage-num">03 · Accept</span>
|
||||
<h3 class="live-mode-stage-name">Lands in real source</h3>
|
||||
<p class="live-mode-stage-desc">The accepted variant replaces the picked element in your source file. CSS consolidates into your real stylesheet, not inline. Discard all three and the original stays.</p>
|
||||
<div class="live-mode-stage-viz">
|
||||
<span class="docs-viz-accept-pill">Variant 2 written to source</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-mode-pathways" aria-label="Where to go next">
|
||||
<h2 class="live-mode-pathways-title">Where next</h2>
|
||||
<div class="live-mode-pathways-grid">
|
||||
<a class="live-mode-pathway" href="/tutorials/iterate-live">
|
||||
<span class="live-mode-pathway-kind">Tutorial</span>
|
||||
<h3 class="live-mode-pathway-title">Walk it step by step</h3>
|
||||
<p class="live-mode-pathway-desc">A ten-minute walkthrough from first run to accepted variant. Covers CSP patching, the picker actions, and the fallback flow for generated files.</p>
|
||||
<span class="live-mode-pathway-cta">Open the tutorial →</span>
|
||||
</a>
|
||||
<a class="live-mode-pathway" href="/docs/live">
|
||||
<span class="live-mode-pathway-kind">Reference</span>
|
||||
<h3 class="live-mode-pathway-title">Full command reference</h3>
|
||||
<p class="live-mode-pathway-desc">Everything your AI harness reads when <code>/impeccable live</code> runs: the poll loop, the wrap/accept helpers, the CSP templates, and every event shape.</p>
|
||||
<span class="live-mode-pathway-cta">Read the reference →</span>
|
||||
</a>
|
||||
<a class="live-mode-pathway" href="/#downloads">
|
||||
<span class="live-mode-pathway-kind">Install</span>
|
||||
<h3 class="live-mode-pathway-title">Get Impeccable set up</h3>
|
||||
<p class="live-mode-pathway-desc">Install the skill and CLI once, then run <code>/impeccable live</code> from your AI harness. Works with Claude Code, Cursor, Codex, Gemini, and more.</p>
|
||||
<span class="live-mode-pathway-cta">See the install steps →</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-mode-frameworks" aria-label="Supported frameworks">
|
||||
<span class="live-mode-frameworks-label">Supported dev servers</span>
|
||||
<ul class="live-mode-frameworks-list">
|
||||
<li>Vite</li>
|
||||
<li>Next.js (incl. monorepos)</li>
|
||||
<li>SvelteKit</li>
|
||||
<li>Astro</li>
|
||||
<li>Nuxt</li>
|
||||
<li>Bun</li>
|
||||
<li>Plain static HTML</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a tutorial detail page main content.
|
||||
*/
|
||||
@@ -647,7 +890,7 @@ ${rules.map(renderRuleCard).join('\n')}
|
||||
<header class="anti-patterns-header">
|
||||
<p class="sub-page-eyebrow">${totalRules} rules</p>
|
||||
<h1 class="sub-page-title">Anti-patterns</h1>
|
||||
<p class="sub-page-lede">The full catalog of patterns <a href="/docs/impeccable">/impeccable</a> teaches against. ${detectedCount} are caught by a deterministic detector (<code>npx impeccable detect</code> or the browser extension). ${llmCount} can only be flagged by <a href="/docs/critique">/impeccable critique</a>'s LLM review pass. Want to see them live on real pages? Try <a href="/visual-mode">Visual Mode</a>, or iterate past them on your own dev server with <a href="/docs/live">Live Mode</a>.</p>
|
||||
<p class="sub-page-lede">The full catalog of patterns <a href="/docs/impeccable">/impeccable</a> teaches against. ${detectedCount} are caught by a deterministic detector (<code>npx impeccable detect</code> or the browser extension). ${llmCount} can only be flagged by <a href="/docs/critique">/impeccable critique</a>'s LLM review pass. Want to see them live on real pages? Try <a href="/visual-mode">Visual Mode</a>, or iterate past them on your own dev server with <a href="/live-mode">Live Mode</a>.</p>
|
||||
</header>
|
||||
|
||||
<details class="anti-patterns-legend">
|
||||
@@ -684,6 +927,7 @@ export async function generateSubPages(rootDir) {
|
||||
antiPatterns: path.join(rootDir, 'public/anti-patterns'),
|
||||
tutorials: path.join(rootDir, 'public/tutorials'),
|
||||
visualMode: path.join(rootDir, 'public/visual-mode'),
|
||||
liveMode: path.join(rootDir, 'public/live-mode'),
|
||||
};
|
||||
|
||||
// Fresh output dirs each time so stale files don't linger.
|
||||
@@ -784,6 +1028,31 @@ export async function generateSubPages(rootDir) {
|
||||
generated.push(out);
|
||||
}
|
||||
|
||||
// Live Mode: marketing landing mirroring /visual-mode. Needs live-mode.css
|
||||
// (not imported by sub-pages.css to keep the base bundle small) and the
|
||||
// live-demo JS module to animate the demo.
|
||||
{
|
||||
const extraHead = `
|
||||
<link rel="stylesheet" href="../css/live-mode.css">
|
||||
<script type="module">
|
||||
import { initLiveDemo } from "../js/components/live-demo.js";
|
||||
document.addEventListener("DOMContentLoaded", initLiveDemo);
|
||||
</script>`;
|
||||
const html = renderPage({
|
||||
title: 'Live Mode | Impeccable',
|
||||
description:
|
||||
'Iterate on UI in the browser. Pick an element, drop a comment, get three production-quality variants, accept one, and it writes back to source. /impeccable live.',
|
||||
bodyHtml: renderLiveModeMain(),
|
||||
activeNav: 'live',
|
||||
canonicalPath: '/live-mode',
|
||||
bodyClass: 'sub-page live-mode-page-body',
|
||||
extraHead,
|
||||
});
|
||||
const out = path.join(outDirs.liveMode, 'index.html');
|
||||
fs.writeFileSync(out, html, 'utf-8');
|
||||
generated.push(out);
|
||||
}
|
||||
|
||||
// Tutorial detail pages.
|
||||
for (const tutorial of data.tutorials) {
|
||||
const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, { kind: 'tutorial', slug: tutorial.slug });
|
||||
|
||||
Reference in New Issue
Block a user