Homepage redesign: show-don't-tell with animated scope hero
Restructure the homepage to lead with visuals instead of text: - Hero: split layout with animated Tektronix 465 oscilloscope showing RC step response (CSS+SVG, zero JS) that links to the notebook - Pipeline strip: 3-step Write → Simulate → Visualize with code/terminal previews and inline waveform SVG - Featured notebooks: 3 curated circuits (RC, 555, common emitter) with pre-rendered waveform thumbnails - Gallery cards: decorative graticule header strip, color-coded by engine - Footer: updated copy with clearer call to action All new sections are server-rendered Astro components. Total new client JavaScript: zero bytes.
This commit is contained in:
parent
3c9c83742d
commit
43789bdf24
87
frontend/src/components/FeaturedNotebooks.astro
Normal file
87
frontend/src/components/FeaturedNotebooks.astro
Normal file
@ -0,0 +1,87 @@
|
||||
---
|
||||
import { rcLowPassSvg, astable555Svg, commonEmitterSvg } from '../lib/featured-waveforms';
|
||||
|
||||
const featured = [
|
||||
{
|
||||
id: 'rc-lowpass-filter',
|
||||
title: 'RC Low-Pass Filter',
|
||||
engine: 'ngspice' as const,
|
||||
description: 'Classic transient analysis — square wave input through an RC network produces the characteristic exponential charge/discharge response.',
|
||||
tags: ['transient', 'passive', 'beginner'],
|
||||
svg: rcLowPassSvg,
|
||||
},
|
||||
{
|
||||
id: '555-astable-blinker',
|
||||
title: '555 Astable LED Blinker',
|
||||
engine: 'ngspice' as const,
|
||||
description: 'Timer IC in free-running mode — the capacitor ramp between comparator thresholds generates a square wave output.',
|
||||
tags: ['timer', 'oscillator', 'digital'],
|
||||
svg: astable555Svg,
|
||||
},
|
||||
{
|
||||
id: 'common-emitter-amplifier',
|
||||
title: 'Common Emitter Amplifier',
|
||||
engine: 'ngspice' as const,
|
||||
description: 'Single-stage BJT amplifier with AC analysis — input sine wave is amplified and phase-inverted at the collector.',
|
||||
tags: ['amplifier', 'ac-analysis', 'bjt'],
|
||||
svg: commonEmitterSvg,
|
||||
},
|
||||
];
|
||||
|
||||
const engineColor: Record<string, { border: string; bg: string; text: string }> = {
|
||||
ngspice: { border: 'border-blue-500/30', bg: 'bg-blue-500/10', text: 'text-blue-400' },
|
||||
ltspice: { border: 'border-amber-500/30', bg: 'bg-amber-500/10', text: 'text-amber-400' },
|
||||
};
|
||||
---
|
||||
|
||||
<section class="max-w-6xl mx-auto px-6 py-16">
|
||||
<p class="text-sm font-semibold tracking-widest text-blue-400 uppercase mb-2">Featured</p>
|
||||
<h2 class="text-2xl font-bold text-slate-100 mb-8">Start with a classic circuit</h2>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{featured.map((nb) => {
|
||||
const colors = engineColor[nb.engine] ?? engineColor.ngspice;
|
||||
return (
|
||||
<a
|
||||
href={`/notebook/${nb.id}`}
|
||||
class:list={[
|
||||
'group block rounded-lg border bg-slate-900/50 overflow-hidden',
|
||||
'hover:border-blue-500/40 hover:bg-slate-800/70 transition-all',
|
||||
'border-slate-800',
|
||||
]}
|
||||
>
|
||||
{/* Waveform thumbnail */}
|
||||
<div class="w-full h-[180px] bg-[#0a0a0a] border-b border-slate-800/60 overflow-hidden">
|
||||
<Fragment set:html={nb.svg} />
|
||||
</div>
|
||||
|
||||
<div class="p-5">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="text-base font-semibold text-slate-100 group-hover:text-blue-400 transition-colors line-clamp-1">
|
||||
{nb.title}
|
||||
</h3>
|
||||
<span class:list={[
|
||||
'shrink-0 ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border',
|
||||
colors.border, colors.bg, colors.text,
|
||||
]}>
|
||||
{nb.engine}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-slate-400 leading-relaxed mb-3 line-clamp-2">
|
||||
{nb.description}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{nb.tags.map((tag) => (
|
||||
<span class="text-xs bg-slate-700/50 text-slate-400 px-2 py-0.5 rounded">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
@ -29,37 +29,40 @@ export function NotebookCard({ notebook }: NotebookCardProps) {
|
||||
return (
|
||||
<a
|
||||
href={`/notebook/${encodeURIComponent(id)}`}
|
||||
className="group block rounded-lg border border-slate-800 bg-slate-900/50 p-5 hover:border-blue-500/40 hover:bg-slate-800/70 transition-all"
|
||||
className="group block rounded-lg border border-slate-800 bg-slate-900/50 overflow-hidden hover:border-blue-500/40 hover:bg-slate-800/70 transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="text-base font-semibold text-slate-100 group-hover:text-blue-400 transition-colors line-clamp-1">
|
||||
{title}
|
||||
</h3>
|
||||
<Badge variant={engineVariant[engine] ?? 'default'} className="shrink-0 ml-2">
|
||||
{engine}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-slate-500 mb-3">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<LayoutGrid className="w-3 h-3" />
|
||||
{cell_count} cell{cell_count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<span>{formatDate(modified)}</span>
|
||||
</div>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs bg-slate-700/50 text-slate-400 px-2 py-0.5 rounded"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
<div className="notebook-card-strip" data-engine={engine} />
|
||||
<div className="p-5">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="text-base font-semibold text-slate-100 group-hover:text-blue-400 transition-colors line-clamp-1">
|
||||
{title}
|
||||
</h3>
|
||||
<Badge variant={engineVariant[engine] ?? 'default'} className="shrink-0 ml-2">
|
||||
{engine}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-slate-500 mb-3">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<LayoutGrid className="w-3 h-3" />
|
||||
{cell_count} cell{cell_count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<span>{formatDate(modified)}</span>
|
||||
</div>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs bg-slate-700/50 text-slate-400 px-2 py-0.5 rounded"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
163
frontend/src/components/PipelineStrip.astro
Normal file
163
frontend/src/components/PipelineStrip.astro
Normal file
@ -0,0 +1,163 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
---
|
||||
|
||||
<section class="max-w-6xl mx-auto px-6 py-16">
|
||||
<div class="grid gap-6 md:grid-cols-[1fr_auto_1fr_auto_1fr] items-start">
|
||||
|
||||
<!-- Step 1: Write -->
|
||||
<div class="pipeline-step">
|
||||
<span class="pipeline-number">01</span>
|
||||
<h3 class="pipeline-title">Write</h3>
|
||||
<div class="pipeline-preview">
|
||||
<pre class="pipeline-code"><span class="text-slate-500">* RC Low-Pass Filter</span>
|
||||
<span class="text-blue-400">V1</span> in 0 PULSE(0 5 0 1n 1n 0.5m 1m)
|
||||
<span class="text-blue-400">R1</span> in out 10k
|
||||
<span class="text-blue-400">C1</span> out 0 100n
|
||||
<span class="text-emerald-400">.tran</span> 10u 5m</pre>
|
||||
</div>
|
||||
<p class="pipeline-desc">
|
||||
SPICE netlists with syntax highlighting and autocomplete
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Arrow 1→2 -->
|
||||
<div class="pipeline-arrow">
|
||||
<Icon name="lucide:arrow-right" class="w-5 h-5 text-slate-600 hidden md:block" />
|
||||
<Icon name="lucide:chevron-down" class="w-5 h-5 text-slate-600 md:hidden" />
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Simulate -->
|
||||
<div class="pipeline-step">
|
||||
<span class="pipeline-number">02</span>
|
||||
<h3 class="pipeline-title">Simulate</h3>
|
||||
<div class="pipeline-preview">
|
||||
<div class="pipeline-terminal">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
|
||||
<span class="text-emerald-400 text-xs font-mono font-semibold">ngspice 43</span>
|
||||
</div>
|
||||
<div class="text-xs font-mono text-slate-500 space-y-0.5">
|
||||
<p>Circuit: RC Low-Pass Filter</p>
|
||||
<p>Doing transient analysis...</p>
|
||||
<p>501 data points, 23ms</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 mt-2">
|
||||
<Icon name="lucide:check-circle-2" class="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span class="text-emerald-400 text-xs font-mono font-semibold">complete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="pipeline-desc">
|
||||
Run ngspice simulations with one click
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Arrow 2→3 -->
|
||||
<div class="pipeline-arrow">
|
||||
<Icon name="lucide:arrow-right" class="w-5 h-5 text-slate-600 hidden md:block" />
|
||||
<Icon name="lucide:chevron-down" class="w-5 h-5 text-slate-600 md:hidden" />
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Visualize -->
|
||||
<div class="pipeline-step">
|
||||
<span class="pipeline-number">03</span>
|
||||
<h3 class="pipeline-title">Visualize</h3>
|
||||
<div class="pipeline-preview pipeline-waveform">
|
||||
<svg viewBox="0 0 300 140" xmlns="http://www.w3.org/2000/svg" class="w-full h-full" preserveAspectRatio="none">
|
||||
<!-- Faint grid -->
|
||||
<defs>
|
||||
<pattern id="ps-grid" width="30" height="17.5" patternUnits="userSpaceOnUse">
|
||||
<line x1="30" y1="0" x2="30" y2="17.5" stroke="rgba(45,212,191,0.06)" stroke-width="0.5"/>
|
||||
<line x1="0" y1="17.5" x2="30" y2="17.5" stroke="rgba(45,212,191,0.06)" stroke-width="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="300" height="140" fill="#0a0a0a"/>
|
||||
<rect width="300" height="140" fill="url(#ps-grid)"/>
|
||||
<!-- Input square wave (dim) -->
|
||||
<polyline fill="none" stroke="rgba(96,165,250,0.3)" stroke-width="1.2"
|
||||
points="0,115 0,25 75,25 75,115 150,115 150,25 225,25 225,115 300,115"/>
|
||||
<!-- RC response (bright teal) -->
|
||||
<path fill="none" stroke="#2dd4bf" stroke-width="1.8" stroke-linecap="round"
|
||||
d="M0,115 C19,50 38,30 75,27 C94,95 112,112 150,114 C169,50 188,30 225,27 C244,95 262,112 300,114"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="pipeline-desc">
|
||||
Interactive waveforms + oscilloscope mode
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.pipeline-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pipeline-number {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--color-sb-accent, #2563eb);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pipeline-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: #f1f5f9;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.pipeline-preview {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #1e293b;
|
||||
background: #0f172a;
|
||||
overflow: hidden;
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.pipeline-code {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
padding: 0.875rem 1rem;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
color: #e2e8f0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pipeline-terminal {
|
||||
padding: 0.875rem 1rem;
|
||||
}
|
||||
|
||||
.pipeline-waveform {
|
||||
padding: 0;
|
||||
aspect-ratio: 300 / 140;
|
||||
}
|
||||
|
||||
.pipeline-desc {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pipeline-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.pipeline-arrow {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
59
frontend/src/lib/featured-waveforms.ts
Normal file
59
frontend/src/lib/featured-waveforms.ts
Normal file
@ -0,0 +1,59 @@
|
||||
// Pre-rendered SVG waveform thumbnails for the 3 featured notebooks.
|
||||
// Each is a self-contained SVG string with dark background, faint grid, and trace paths.
|
||||
|
||||
const GRID = `<defs>
|
||||
<pattern id="ft-grid" width="10%" height="12.5%" patternUnits="objectBoundingBox">
|
||||
<line x1="100%" y1="0" x2="100%" y2="100%" stroke="rgba(45,212,191,0.06)" stroke-width="0.5"/>
|
||||
<line x1="0" y1="100%" x2="100%" y2="100%" stroke="rgba(45,212,191,0.06)" stroke-width="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#0a0a0a"/>
|
||||
<rect width="100%" height="100%" fill="url(#ft-grid)"/>`;
|
||||
|
||||
// 1. RC Low-Pass Filter — square wave input (dim) + exponential charge/discharge output (bright)
|
||||
// Time axis: 0→5ms, two full cycles of 1kHz square wave through R=10k, C=100nF (τ=1ms)
|
||||
export const rcLowPassSvg = `<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
|
||||
${GRID}
|
||||
<!-- Input: square wave (dimmer) -->
|
||||
<polyline fill="none" stroke="rgba(96,165,250,0.3)" stroke-width="1.5"
|
||||
points="0,160 0,40 100,40 100,160 200,160 200,40 300,40 300,160 400,160"/>
|
||||
<!-- Output: RC exponential response (bright teal) -->
|
||||
<path fill="none" stroke="#2dd4bf" stroke-width="2" stroke-linecap="round"
|
||||
d="M0,160
|
||||
C25,70 50,45 100,42
|
||||
C125,130 150,155 200,158
|
||||
C225,70 250,45 300,42
|
||||
C325,130 350,155 400,158"/>
|
||||
</svg>`;
|
||||
|
||||
// 2. 555 Astable LED Blinker — sharp square wave output with slight ramp on edges
|
||||
// Output toggles between ~0V and ~Vcc with RC timing visible
|
||||
export const astable555Svg = `<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
|
||||
${GRID}
|
||||
<!-- Capacitor voltage: sawtooth ramp between thresholds (dimmer) -->
|
||||
<polyline fill="none" stroke="rgba(250,204,21,0.3)" stroke-width="1.5"
|
||||
points="0,130 70,80 70,130 140,80 140,130 210,80 210,130 280,80 280,130 350,80 350,130 400,95"/>
|
||||
<!-- Output: clean square wave (bright amber) -->
|
||||
<polyline fill="none" stroke="#f59e0b" stroke-width="2"
|
||||
points="0,160 0,40 70,40 70,160 140,160 140,40 210,40 210,160 280,160 280,40 350,40 350,160 400,160"/>
|
||||
</svg>`;
|
||||
|
||||
// 3. Common Emitter Amplifier — input sine (dim) + amplified inverted sine (bright)
|
||||
// AC analysis showing voltage gain with phase inversion
|
||||
export const commonEmitterSvg = `<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
|
||||
${GRID}
|
||||
<!-- Input: small sine wave centered (dimmer) -->
|
||||
<path fill="none" stroke="rgba(167,139,250,0.35)" stroke-width="1.5"
|
||||
d="M0,100
|
||||
C17,85 33,85 50,100 C67,115 83,115 100,100
|
||||
C117,85 133,85 150,100 C167,115 183,115 200,100
|
||||
C217,85 233,85 250,100 C267,115 283,115 300,100
|
||||
C317,85 333,85 350,100 C367,115 383,115 400,100"/>
|
||||
<!-- Output: amplified, inverted sine (bright green) -->
|
||||
<path fill="none" stroke="#34d399" stroke-width="2" stroke-linecap="round"
|
||||
d="M0,100
|
||||
C17,145 33,145 50,100 C67,55 83,55 100,100
|
||||
C117,145 133,145 150,100 C167,55 183,55 200,100
|
||||
C217,145 233,145 250,100 C267,55 283,55 300,100
|
||||
C317,145 333,145 350,100 C367,55 383,55 400,100"/>
|
||||
</svg>`;
|
||||
@ -2,6 +2,8 @@
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import NotebookLayout from '../layouts/NotebookLayout.astro';
|
||||
import NotebookGallery from '../components/NotebookGallery';
|
||||
import PipelineStrip from '../components/PipelineStrip.astro';
|
||||
import FeaturedNotebooks from '../components/FeaturedNotebooks.astro';
|
||||
import { fetchNotebookList } from '../lib/server-api';
|
||||
import type { NotebookSummary } from '../lib/types';
|
||||
|
||||
@ -23,72 +25,139 @@ try {
|
||||
@import '../styles/homepage.css';
|
||||
</style>
|
||||
|
||||
<!-- Hero -->
|
||||
<!-- Hero: Split layout — text left, animated scope visual right -->
|
||||
<section class="hero-graticule border-b border-slate-800/60">
|
||||
<div class="relative max-w-6xl mx-auto px-6 pt-16 pb-20">
|
||||
<p class="text-sm font-semibold tracking-widest text-blue-400 uppercase mb-4">SpiceBook</p>
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-slate-100 tracking-tight max-w-2xl">
|
||||
Circuit Simulation Notebooks
|
||||
</h1>
|
||||
<p class="mt-4 text-lg text-slate-400 max-w-xl leading-relaxed">
|
||||
Write SPICE netlists, run ngspice simulations, and visualize waveforms in a single document.
|
||||
</p>
|
||||
<div class="flex items-center gap-3 mt-8">
|
||||
<a
|
||||
href="/notebook/new"
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors text-sm"
|
||||
>
|
||||
<Icon name="lucide:plus" class="w-4 h-4" />
|
||||
New Notebook
|
||||
</a>
|
||||
<a
|
||||
href="#notebooks"
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg border border-slate-700 text-slate-300 hover:border-slate-500 hover:text-slate-100 font-medium transition-colors text-sm"
|
||||
>
|
||||
Browse Notebooks
|
||||
<Icon name="lucide:chevron-down" class="w-4 h-4" />
|
||||
</a>
|
||||
<div class="grid md:grid-cols-2 gap-12 items-center">
|
||||
|
||||
<!-- Left: Copy + CTAs -->
|
||||
<div>
|
||||
<p class="text-sm font-semibold tracking-widest text-blue-400 uppercase mb-4">SpiceBook</p>
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-slate-100 tracking-tight">
|
||||
Circuit Simulation Notebooks
|
||||
</h1>
|
||||
<p class="mt-4 text-lg text-slate-400 max-w-xl leading-relaxed">
|
||||
Write SPICE netlists, run ngspice simulations, and visualize waveforms in a single document.
|
||||
</p>
|
||||
<div class="flex items-center gap-3 mt-8">
|
||||
<a
|
||||
href="/notebook/new"
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors text-sm"
|
||||
>
|
||||
<Icon name="lucide:plus" class="w-4 h-4" />
|
||||
New Notebook
|
||||
</a>
|
||||
<a
|
||||
href="#notebooks"
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg border border-slate-700 text-slate-300 hover:border-slate-500 hover:text-slate-100 font-medium transition-colors text-sm"
|
||||
>
|
||||
Browse Notebooks
|
||||
<Icon name="lucide:chevron-down" class="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Animated oscilloscope visual -->
|
||||
<div class="flex justify-center md:justify-end">
|
||||
<a href="/notebook/rc-lowpass-filter" class="hero-scope" aria-label="Open RC Low-Pass Filter notebook">
|
||||
<div class="hero-scope-frame">
|
||||
|
||||
<!-- Brand bar -->
|
||||
<div class="hero-scope-brand">
|
||||
<span class="hero-scope-brand-name">Tektronix</span>
|
||||
<span class="hero-scope-brand-model">465</span>
|
||||
</div>
|
||||
|
||||
<!-- CRT bay -->
|
||||
<div class="hero-scope-crt-bay">
|
||||
<div class="hero-scope-screen">
|
||||
<!-- Waveform SVG: RC step response -->
|
||||
<svg viewBox="0 0 500 300" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none" aria-hidden="true">
|
||||
<!-- Input: square wave (dim blue) -->
|
||||
<polyline
|
||||
class="hero-scope-trace-input"
|
||||
fill="none"
|
||||
stroke="rgba(96,165,250,0.25)"
|
||||
stroke-width="1.5"
|
||||
points="0,240 0,60 125,60 125,240 250,240 250,60 375,60 375,240 500,240"
|
||||
/>
|
||||
<!-- Output: RC exponential response (bright teal) -->
|
||||
<path
|
||||
class="hero-scope-trace-output"
|
||||
fill="none"
|
||||
stroke="#2dd4bf"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
d="M0,240
|
||||
C31,105 62,68 125,62
|
||||
C156,195 188,232 250,238
|
||||
C281,105 312,68 375,62
|
||||
C406,195 438,232 500,238"
|
||||
/>
|
||||
</svg>
|
||||
<!-- Graticule + scanlines -->
|
||||
<div class="hero-scope-graticule"></div>
|
||||
<div class="hero-scope-scanlines"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Digital readout -->
|
||||
<div class="hero-scope-readout">
|
||||
<span>V(out)</span>
|
||||
<span class="hero-scope-readout-divider">|</span>
|
||||
<span>200μs/div</span>
|
||||
<span class="hero-scope-readout-divider">|</span>
|
||||
<span>500mV/div</span>
|
||||
</div>
|
||||
|
||||
<!-- Control panel -->
|
||||
<div class="hero-scope-panel">
|
||||
<div class="hero-scope-controls">
|
||||
<div class="hero-scope-section">
|
||||
<span class="hero-scope-section-label">Volts</span>
|
||||
<div class="hero-scope-knob" data-pos="2"></div>
|
||||
</div>
|
||||
<div class="hero-scope-section">
|
||||
<span class="hero-scope-section-label">Time</span>
|
||||
<div class="hero-scope-knob" data-pos="3"></div>
|
||||
</div>
|
||||
<div class="hero-scope-section">
|
||||
<span class="hero-scope-section-label">Trigger</span>
|
||||
<div class="hero-scope-knob" data-pos="1"></div>
|
||||
</div>
|
||||
<div class="hero-scope-section">
|
||||
<div class="hero-scope-mode">TRAN</div>
|
||||
<div class="hero-scope-led"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Feature Highlights -->
|
||||
<section class="max-w-6xl mx-auto px-6 py-16">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="p-5 rounded-lg border border-slate-800 bg-slate-900/50">
|
||||
<Icon name="lucide:zap" class="w-6 h-6 text-blue-400 mb-3" />
|
||||
<h3 class="font-semibold text-slate-100 mb-1">SPICE Simulation</h3>
|
||||
<p class="text-sm text-slate-400 leading-relaxed">
|
||||
Write netlists and run ngspice. Transient, AC, DC sweep, operating point.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-5 rounded-lg border border-slate-800 bg-slate-900/50">
|
||||
<Icon name="lucide:activity" class="w-6 h-6 text-blue-400 mb-3" />
|
||||
<h3 class="font-semibold text-slate-100 mb-1">Waveform Plots</h3>
|
||||
<p class="text-sm text-slate-400 leading-relaxed">
|
||||
Results render as interactive plots below each netlist.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-5 rounded-lg border border-slate-800 bg-slate-900/50">
|
||||
<Icon name="lucide:circuit-board" class="w-6 h-6 text-blue-400 mb-3" />
|
||||
<h3 class="font-semibold text-slate-100 mb-1">Schematic Generation</h3>
|
||||
<p class="text-sm text-slate-400 leading-relaxed">
|
||||
Netlists produce SVG circuit diagrams automatically.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-5 rounded-lg border border-slate-800 bg-slate-900/50">
|
||||
<Icon name="lucide:file-text" class="w-6 h-6 text-blue-400 mb-3" />
|
||||
<h3 class="font-semibold text-slate-100 mb-1">Notebook Format</h3>
|
||||
<p class="text-sm text-slate-400 leading-relaxed">
|
||||
Markdown, SPICE, and Python cells in a single document.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pipeline: Write → Simulate → Visualize -->
|
||||
<section class="border-b border-slate-800/60">
|
||||
<PipelineStrip />
|
||||
</section>
|
||||
|
||||
<!-- Featured Notebooks -->
|
||||
<section class="border-b border-slate-800/60">
|
||||
<FeaturedNotebooks />
|
||||
</section>
|
||||
|
||||
<!-- Notebook Gallery -->
|
||||
<section id="notebooks" class="max-w-6xl mx-auto px-6 pb-20 scroll-mt-8">
|
||||
<h2 class="text-2xl font-bold text-slate-100 mb-8">Notebooks</h2>
|
||||
<section id="notebooks" class="max-w-6xl mx-auto px-6 py-20 scroll-mt-8">
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-slate-100">Explore Notebooks</h2>
|
||||
{notebooks.length > 0 && (
|
||||
<p class="text-sm text-slate-500 mt-1">{notebooks.length} notebooks available</p>
|
||||
)}
|
||||
</div>
|
||||
<NotebookGallery
|
||||
client:load
|
||||
initialNotebooks={notebooks}
|
||||
@ -99,10 +168,11 @@ try {
|
||||
<!-- Footer CTA -->
|
||||
<footer class="border-t border-slate-800/60">
|
||||
<div class="max-w-6xl mx-auto px-6 py-16 text-center">
|
||||
<h2 class="text-2xl font-bold text-slate-100 mb-3">Ready to simulate?</h2>
|
||||
<h2 class="text-2xl font-bold text-slate-100 mb-2">Start with a blank notebook</h2>
|
||||
<p class="text-sm text-slate-400 mb-6">Write a SPICE netlist, run it, see results in seconds.</p>
|
||||
<a
|
||||
href="/notebook/new"
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors text-sm mt-4"
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors text-sm"
|
||||
>
|
||||
<Icon name="lucide:plus" class="w-4 h-4" />
|
||||
New Notebook
|
||||
|
||||
@ -29,3 +29,418 @@
|
||||
.filter-pills::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
Hero Oscilloscope Visual — Pure CSS + SVG
|
||||
Adapted from scope-skin.css, fully independent (hero-scope- prefix)
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Color palette */
|
||||
.hero-scope {
|
||||
--hs-teal: #2dd4bf;
|
||||
--hs-teal-dim: rgba(45, 212, 191, 0.12);
|
||||
--hs-teal-glow: rgba(45, 212, 191, 0.18);
|
||||
--hs-panel: #b5a48a;
|
||||
--hs-panel-light: #c7b89e;
|
||||
--hs-panel-dark: #9e8f78;
|
||||
--hs-crt-bg: #0a0a0a;
|
||||
--hs-label: #3b3428;
|
||||
--hs-knob: #2a2a2d;
|
||||
--hs-knob-ring: #1e1e20;
|
||||
--hs-section-line: rgba(59, 52, 40, 0.25);
|
||||
|
||||
display: block;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Outer frame ──────────────────────────────────────────── */
|
||||
.hero-scope-frame {
|
||||
position: relative;
|
||||
background:
|
||||
url("data:image/svg+xml,%3Csvg width='4' height='4' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1' x='0' y='0' fill='rgba(0,0,0,0.03)'/%3E%3Crect width='1' height='1' x='2' y='2' fill='rgba(255,255,255,0.02)'/%3E%3C/svg%3E"),
|
||||
linear-gradient(175deg, var(--hs-panel-light), var(--hs-panel), var(--hs-panel-dark));
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 8px 30px rgba(0, 0, 0, 0.45),
|
||||
0 2px 6px rgba(0, 0, 0, 0.25),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* ── Brand bar ────────────────────────────────────────────── */
|
||||
.hero-scope-brand {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
padding: 8px 14px 6px;
|
||||
border-bottom: 1px solid var(--hs-section-line);
|
||||
}
|
||||
|
||||
.hero-scope-brand-name {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--hs-label);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.hero-scope-brand-model {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.55rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--hs-label);
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
/* ── CRT bay ──────────────────────────────────────────────── */
|
||||
.hero-scope-crt-bay {
|
||||
margin: 10px 12px 0;
|
||||
background: #1a1816;
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
box-shadow:
|
||||
inset 0 2px 6px rgba(0, 0, 0, 0.5),
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* ── CRT screen ───────────────────────────────────────────── */
|
||||
.hero-scope-screen {
|
||||
position: relative;
|
||||
background: var(--hs-crt-bg);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 5 / 3;
|
||||
box-shadow:
|
||||
0 0 15px var(--hs-teal-glow),
|
||||
inset 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* SVG fills the CRT */
|
||||
.hero-scope-screen svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ── Graticule overlay (10×8 grid) ────────────────────────── */
|
||||
.hero-scope-graticule {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
transparent calc(10% - 0.5px),
|
||||
rgba(45, 212, 191, 0.07) calc(10% - 0.5px),
|
||||
rgba(45, 212, 191, 0.07) calc(10% + 0.5px),
|
||||
transparent calc(10% + 0.5px)
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent calc(12.5% - 0.5px),
|
||||
rgba(45, 212, 191, 0.07) calc(12.5% - 0.5px),
|
||||
rgba(45, 212, 191, 0.07) calc(12.5% + 0.5px),
|
||||
transparent calc(12.5% + 0.5px)
|
||||
);
|
||||
}
|
||||
|
||||
/* Center crosshair ticks */
|
||||
.hero-scope-graticule::before,
|
||||
.hero-scope-graticule::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.hero-scope-graticule::before {
|
||||
top: 50%;
|
||||
left: calc(50% - 8px);
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
background: rgba(45, 212, 191, 0.18);
|
||||
}
|
||||
|
||||
.hero-scope-graticule::after {
|
||||
left: 50%;
|
||||
top: calc(50% - 8px);
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: rgba(45, 212, 191, 0.18);
|
||||
}
|
||||
|
||||
/* ── Scanlines ────────────────────────────────────────────── */
|
||||
.hero-scope-scanlines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0, 0, 0, 0.04) 2px,
|
||||
rgba(0, 0, 0, 0.04) 4px
|
||||
);
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
/* ── SVG trace animation ──────────────────────────────────── */
|
||||
.hero-scope-trace-input {
|
||||
stroke-dasharray: 1200;
|
||||
stroke-dashoffset: 1200;
|
||||
animation: hero-trace-draw 1.8s ease-out 0.3s forwards;
|
||||
}
|
||||
|
||||
.hero-scope-trace-output {
|
||||
stroke-dasharray: 1200;
|
||||
stroke-dashoffset: 1200;
|
||||
animation: hero-trace-draw 1.8s ease-out 0.6s forwards;
|
||||
}
|
||||
|
||||
@keyframes hero-trace-draw {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Phosphor glow pulse after draw completes */
|
||||
.hero-scope-trace-output {
|
||||
animation:
|
||||
hero-trace-draw 1.8s ease-out 0.6s forwards,
|
||||
hero-phosphor-pulse 3s ease-in-out 2.4s infinite;
|
||||
}
|
||||
|
||||
@keyframes hero-phosphor-pulse {
|
||||
0%, 100% {
|
||||
filter: drop-shadow(0 0 2px var(--hs-teal-glow));
|
||||
}
|
||||
50% {
|
||||
filter: drop-shadow(0 0 6px rgba(45, 212, 191, 0.35));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Digital readout bar ──────────────────────────────────── */
|
||||
.hero-scope-readout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 5px 12px;
|
||||
background: #0a0a0a;
|
||||
margin: 0 12px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--hs-teal);
|
||||
text-shadow: 0 0 4px var(--hs-teal-glow);
|
||||
}
|
||||
|
||||
.hero-scope-readout-divider {
|
||||
color: rgba(45, 212, 191, 0.25);
|
||||
}
|
||||
|
||||
/* ── Control panel ────────────────────────────────────────── */
|
||||
.hero-scope-panel {
|
||||
padding: 8px 12px 8px;
|
||||
border-top: 1px solid var(--hs-section-line);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.hero-scope-controls {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.hero-scope-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Section dividers */
|
||||
.hero-scope-section + .hero-scope-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--hs-section-line);
|
||||
}
|
||||
|
||||
.hero-scope-section-label {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--hs-label);
|
||||
opacity: 0.88;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ── Decorative knobs ─────────────────────────────────────── */
|
||||
.hero-scope-knob {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 40% 35%, #3a3a3e, var(--hs-knob));
|
||||
border: 2px solid var(--hs-knob-ring);
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.35),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.06);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Knob indicator line */
|
||||
.hero-scope-knob::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 50%;
|
||||
width: 1.5px;
|
||||
height: 7px;
|
||||
background: #d4d0c8;
|
||||
border-radius: 1px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* Knob rotation variants */
|
||||
.hero-scope-knob[data-pos="1"] { transform: rotate(-60deg); }
|
||||
.hero-scope-knob[data-pos="2"] { transform: rotate(-20deg); }
|
||||
.hero-scope-knob[data-pos="3"] { transform: rotate(30deg); }
|
||||
.hero-scope-knob[data-pos="4"] { transform: rotate(70deg); }
|
||||
|
||||
/* ── Mode badge ───────────────────────────────────────────── */
|
||||
.hero-scope-mode {
|
||||
background: var(--hs-knob);
|
||||
border: 2px solid var(--hs-knob-ring);
|
||||
border-radius: 6px;
|
||||
color: var(--hs-teal);
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.55rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 5px 8px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.3),
|
||||
0 0 6px var(--hs-teal-glow);
|
||||
}
|
||||
|
||||
/* ── Power LED ────────────────────────────────────────────── */
|
||||
.hero-scope-led {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--hs-teal);
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 0 6px var(--hs-teal-glow);
|
||||
}
|
||||
|
||||
/* ── Accessibility: reduced motion ────────────────────────── */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-scope-trace-input,
|
||||
.hero-scope-trace-output {
|
||||
stroke-dasharray: none;
|
||||
stroke-dashoffset: 0;
|
||||
animation: none;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.hero-scope-scanlines {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Responsive ───────────────────────────────────────────── */
|
||||
@media (max-width: 50rem) {
|
||||
.hero-scope-knob {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.hero-scope-knob::after {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.hero-scope-readout {
|
||||
font-size: 0.55rem;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Gallery card decorative header strip ─────────────────── */
|
||||
.notebook-card-strip {
|
||||
height: 3rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.notebook-card-strip::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
transparent calc(10% - 0.5px),
|
||||
var(--strip-accent, rgba(37, 99, 235, 0.08)) calc(10% - 0.5px),
|
||||
var(--strip-accent, rgba(37, 99, 235, 0.08)) calc(10% + 0.5px),
|
||||
transparent calc(10% + 0.5px)
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent calc(25% - 0.5px),
|
||||
var(--strip-accent, rgba(37, 99, 235, 0.08)) calc(25% - 0.5px),
|
||||
var(--strip-accent, rgba(37, 99, 235, 0.08)) calc(25% + 0.5px),
|
||||
transparent calc(25% + 0.5px)
|
||||
);
|
||||
}
|
||||
|
||||
/* Bottom accent line */
|
||||
.notebook-card-strip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
var(--strip-line, rgba(37, 99, 235, 0.25)) 20%,
|
||||
var(--strip-line, rgba(37, 99, 235, 0.25)) 80%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
/* Engine color variants */
|
||||
.notebook-card-strip[data-engine="ngspice"] {
|
||||
--strip-accent: rgba(37, 99, 235, 0.08);
|
||||
--strip-line: rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
|
||||
.notebook-card-strip[data-engine="ltspice"] {
|
||||
--strip-accent: rgba(245, 158, 11, 0.08);
|
||||
--strip-line: rgba(245, 158, 11, 0.25);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user