feat: polish spectra lab editorial details

This commit is contained in:
Chen Gu
2026-04-23 12:18:35 +08:00
parent 969f0738c4
commit 516528ed73
4 changed files with 140 additions and 6 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
spectra-lab-local.png spectra-lab-local.png
spectra-lab-redesign.png spectra-lab-redesign.png
spectra-lab-polish.png
.DS_Store .DS_Store

38
app.js
View File

@@ -4,12 +4,41 @@ const workItems = document.querySelectorAll('.work-item');
const workIndex = document.getElementById('workIndex'); const workIndex = document.getElementById('workIndex');
const workTitle = document.getElementById('workTitle'); const workTitle = document.getElementById('workTitle');
const workCopy = document.getElementById('workCopy'); const workCopy = document.getElementById('workCopy');
const stageCard = document.querySelector('.stage-card');
const revealBlocks = document.querySelectorAll('.reveal'); const revealBlocks = document.querySelectorAll('.reveal');
const storedMode = window.localStorage.getItem('spectra-mode');
if (storedMode === 'light') {
document.body.classList.add('light-mode');
}
function applyMode() { function applyMode() {
const isLight = document.body.classList.contains('light-mode'); const isLight = document.body.classList.contains('light-mode');
modeToggle.textContent = isLight ? 'Dark Mode' : 'Light Mode'; modeToggle.textContent = isLight ? 'Dark Mode' : 'Light Mode';
modeLabel.textContent = isLight ? 'Editorial Light' : 'Editorial Dark'; modeLabel.textContent = isLight ? 'Editorial Light' : 'Editorial Dark';
window.localStorage.setItem('spectra-mode', isLight ? 'light' : 'dark');
}
function updateWorkStage(item) {
workItems.forEach((button) => {
button.classList.remove('is-active');
button.setAttribute('aria-selected', 'false');
});
item.classList.add('is-active');
item.setAttribute('aria-selected', 'true');
stageCard?.classList.remove('is-refreshing');
requestAnimationFrame(() => {
stageCard?.classList.add('is-refreshing');
window.setTimeout(() => {
stageCard?.classList.remove('is-refreshing');
}, 340);
});
workIndex.textContent = item.dataset.index || '';
workTitle.textContent = item.dataset.title || '';
workCopy.textContent = item.dataset.copy || '';
} }
modeToggle?.addEventListener('click', () => { modeToggle?.addEventListener('click', () => {
@@ -19,11 +48,7 @@ modeToggle?.addEventListener('click', () => {
workItems.forEach((item) => { workItems.forEach((item) => {
item.addEventListener('click', () => { item.addEventListener('click', () => {
workItems.forEach((button) => button.classList.remove('is-active')); updateWorkStage(item);
item.classList.add('is-active');
workIndex.textContent = item.dataset.index || '';
workTitle.textContent = item.dataset.title || '';
workCopy.textContent = item.dataset.copy || '';
}); });
}); });
@@ -40,4 +65,7 @@ const observer = new IntersectionObserver(
); );
revealBlocks.forEach((block) => observer.observe(block)); revealBlocks.forEach((block) => observer.observe(block));
if (workItems[0]) {
updateWorkStage(workItems[0]);
}
applyMode(); applyMode();

View File

@@ -61,6 +61,20 @@
</p> </p>
</aside> </aside>
</div> </div>
<div class="hero-ledger">
<article class="ledger-item">
<span>Design principle</span>
<strong>Less gesture, more judgment.</strong>
</article>
<article class="ledger-item">
<span>Material cue</span>
<strong>纸感、墨感、少量暗红,而不是数字炫光。</strong>
</article>
<article class="ledger-item">
<span>Reading rhythm</span>
<strong>先标题,再说明,再留下停顿。</strong>
</article>
</div>
</section> </section>
<section id="statement" class="statement section reveal"> <section id="statement" class="statement section reveal">
@@ -187,6 +201,10 @@
<p> <p>
如果第一页就把所有招式用完,观者只会记得“效果很多”。但如果页面知道何时收、何时留白、何时只用一条线和一块色面说话,它才更像真正完成度高的作品。 如果第一页就把所有招式用完,观者只会记得“效果很多”。但如果页面知道何时收、何时留白、何时只用一条线和一块色面说话,它才更像真正完成度高的作品。
</p> </p>
<div class="closing-signoff">
<span>Spectra Lab / Editorial pass</span>
<span>Built for calm impact.</span>
</div>
</div> </div>
</section> </section>
</main> </main>

View File

@@ -86,11 +86,16 @@ a {
} }
.topbar { .topbar {
position: sticky;
top: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
padding: 28px 0 20px; padding: 22px 0 18px;
background: linear-gradient(180deg, color-mix(in srgb, var(--bg) 82%, transparent 18%), color-mix(in srgb, var(--bg) 58%, transparent 42%));
backdrop-filter: blur(14px);
z-index: 8;
} }
.brand { .brand {
@@ -141,10 +146,47 @@ a {
color: var(--text); color: var(--text);
} }
.mode-toggle:hover {
border-color: var(--line-strong);
background: var(--accent-soft);
}
.hero { .hero {
padding: 42px 0 72px; padding: 42px 0 72px;
} }
.hero-ledger {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-top: 30px;
padding-top: 18px;
border-top: 1px solid var(--line);
}
.ledger-item {
padding: 18px 18px 20px;
border: 1px solid var(--line);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
}
.ledger-item span,
.closing-signoff span {
display: block;
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.72rem;
color: var(--muted);
}
.ledger-item strong {
display: block;
margin-top: 14px;
font-size: 1rem;
line-height: 1.6;
font-weight: 600;
}
.hero-meta, .hero-meta,
.section-head, .section-head,
.footer { .footer {
@@ -274,6 +316,11 @@ a {
background: var(--accent-soft); background: var(--accent-soft);
} }
.work-item.is-active .work-number,
.work-item.is-active small {
color: var(--text);
}
.work-number { .work-number {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--muted); color: var(--muted);
@@ -299,17 +346,32 @@ a {
} }
.stage-card { .stage-card {
position: relative;
min-height: 100%; min-height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
padding: 34px; padding: 34px;
overflow: hidden;
background: background:
linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.06) 100%), linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.06) 100%),
linear-gradient(135deg, rgba(255,255,255,0.02), transparent 48%), linear-gradient(135deg, rgba(255,255,255,0.02), transparent 48%),
linear-gradient(180deg, var(--panel), color-mix(in srgb, var(--panel) 80%, black 20%)); linear-gradient(180deg, var(--panel), color-mix(in srgb, var(--panel) 80%, black 20%));
} }
.stage-card::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(180deg, transparent 0%, rgba(255, 255, 255, 0.04) 100%);
opacity: 0.5;
}
.stage-card.is-refreshing {
animation: stageRefresh 320ms ease;
}
.stage-index { .stage-index {
font-size: clamp(4.2rem, 8vw, 8rem); font-size: clamp(4.2rem, 8vw, 8rem);
line-height: 0.92; line-height: 0.92;
@@ -354,6 +416,15 @@ a {
color: rgba(20, 20, 20, 0.7); color: rgba(20, 20, 20, 0.7);
} }
.closing-signoff {
display: flex;
justify-content: space-between;
gap: 16px;
margin-top: 30px;
padding-top: 18px;
border-top: 1px solid rgba(20, 20, 20, 0.12);
}
.footer { .footer {
padding: 22px 0 42px; padding: 22px 0 42px;
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
@@ -370,12 +441,24 @@ a {
transform: translateY(0); transform: translateY(0);
} }
@keyframes stageRefresh {
0% {
transform: translateY(8px);
opacity: 0.86;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@media (max-width: 1100px) { @media (max-width: 1100px) {
.topbar, .topbar,
.hero-meta, .hero-meta,
.section-head, .section-head,
.works-layout, .works-layout,
.hero-grid, .hero-grid,
.hero-ledger,
.statement-grid, .statement-grid,
.system-grid, .system-grid,
.footer { .footer {
@@ -435,4 +518,8 @@ a {
.closing-panel { .closing-panel {
padding: 24px; padding: 24px;
} }
.closing-signoff {
flex-direction: column;
}
} }