diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index dceda60..fa7458a 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -10,6 +10,9 @@ export default defineConfig({ starlight({ title: 'mcltspice', tagline: 'LTspice circuit simulation via MCP', + components: { + Hero: './src/components/Hero.astro', + }, social: [ { icon: 'external', label: 'Gitea', href: 'https://git.supported.systems/MCP/mcltspice' }, { icon: 'external', label: 'PyPI', href: 'https://pypi.org/project/mcltspice/' }, diff --git a/docs/public/spirals_shrt.mp3 b/docs/public/spirals_shrt.mp3 new file mode 100644 index 0000000..7f00e84 Binary files /dev/null and b/docs/public/spirals_shrt.mp3 differ diff --git a/docs/src/components/Hero.astro b/docs/src/components/Hero.astro new file mode 100644 index 0000000..4dea1c0 --- /dev/null +++ b/docs/src/components/Hero.astro @@ -0,0 +1,156 @@ +--- +/** + * Custom Hero override for Starlight. + * On splash pages: injects the oscilloscope display in the image area. + * On other pages: delegates to the default Starlight Hero. + */ +import { Image } from 'astro:assets'; +import { LinkButton } from '@astrojs/starlight/components'; +import OscilloscopeDisplay from './OscilloscopeDisplay.astro'; + +const PAGE_TITLE_ID = '_top'; + +const { data } = Astro.locals.starlightRoute.entry; +const { title = data.title, tagline, image, actions = [] } = data.hero || {}; +const isSplash = data.template === 'splash'; + +const imageAttrs = { + loading: 'eager' as const, + decoding: 'async' as const, + width: 400, + height: 400, + alt: image?.alt || '', +}; + +let darkImage: ImageMetadata | undefined; +let lightImage: ImageMetadata | undefined; +let rawHtml: string | undefined; + +if (image) { + if ('file' in image) { + darkImage = image.file; + } else if ('dark' in image) { + darkImage = image.dark; + lightImage = image.light; + } else { + rawHtml = image.html; + } +} +--- + +
+ {isSplash ? ( +
+ +
+ ) : ( + <> + {darkImage && ( + + )} + {lightImage && } + {rawHtml &&
} + + )} +
+
+

+ {tagline &&
} +
+ {actions.length > 0 && ( +
+ {actions.map( + ({ attrs: { class: className, ...attrs } = {}, icon, link: href, text, variant }) => ( + + {text} + {icon?.html && } + + ) + )} +
+ )} +

+
+ + diff --git a/docs/src/components/OscilloscopeDisplay.astro b/docs/src/components/OscilloscopeDisplay.astro new file mode 100644 index 0000000..34c8cb5 --- /dev/null +++ b/docs/src/components/OscilloscopeDisplay.astro @@ -0,0 +1,256 @@ +--- +/** + * XY-mode audio oscilloscope display. + * Visual style inspired by the Tektronix 465 (1972). + * Renders stereo audio as Lissajous patterns on a canvas. + * + * Audio: "Spirals" by Jerobeam Fenderson (oscilloscopemusic.com) + * License: CC BY-NC-SA 4.0 + * Original visual: Nick Watton (codepen.io/2Mogs) + * Gist: rsp2k (gist.github.com/rsp2k/ac68b1bb290b8124e162987ed1df8d53) + * Modernized: ScriptProcessor → AnalyserNode + rAF + */ +--- + +
+ +
+ Tektronix + mcltspice +
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+ + + Volts/Div +
+ + +
+ + + Time/Div +
+ + +
+ + + +
+
+
+ + +
+
+ "Spirals" by Jerobeam Fenderson + (CC BY-NC-SA) + · Visual: Nick Watton + · rsp2k +
+
+
+ + + + diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css index 4370c28..91b7b76 100644 --- a/docs/src/styles/custom.css +++ b/docs/src/styles/custom.css @@ -3,6 +3,7 @@ @import "@astrojs/starlight-tailwind"; @import "tailwindcss"; +@import "./oscilloscope.css"; :root { --sl-color-accent-low: #083344; /* teal-950 */ diff --git a/docs/src/styles/oscilloscope.css b/docs/src/styles/oscilloscope.css new file mode 100644 index 0000000..bc443b9 --- /dev/null +++ b/docs/src/styles/oscilloscope.css @@ -0,0 +1,343 @@ +/* Oscilloscope display -- Tektronix 465 inspired + * Tan/champagne panel, recessed CRT, teal phosphor + * Designed for mcltspice docs hero section */ + +/* ── Outer chassis ───────────────────────────────────────── */ +.scope-frame { + --scope-teal: #2dd4bf; + --scope-teal-dim: rgba(45, 212, 191, 0.12); + --scope-teal-glow: rgba(45, 212, 191, 0.18); + --scope-panel: #b5a48a; + --scope-panel-light: #c7b89e; + --scope-panel-dark: #9e8f78; + --scope-crt-bg: #0a0a0a; + --scope-label: #3b3428; + --scope-knob: #2a2a2d; + --scope-knob-ring: #1e1e20; + --scope-section-line: rgba(59, 52, 40, 0.25); + + position: relative; + background: + /* subtle metallic grain */ + 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(--scope-panel-light), var(--scope-panel), var(--scope-panel-dark)); + border-radius: 6px; + padding: 0; + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.55), + 0 2px 6px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.15), + inset 0 -1px 0 rgba(0, 0, 0, 0.1); + max-width: 340px; + width: 100%; + margin: 0 auto; + overflow: hidden; +} + +/* ── Top brand bar ───────────────────────────────────────── */ +.scope-brand { + display: flex; + align-items: baseline; + justify-content: space-between; + padding: 8px 14px 6px; + border-bottom: 1px solid var(--scope-section-line); +} + +.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(--scope-label); + opacity: 0.75; +} + +.scope-brand-model { + font-family: var(--sl-font-mono, ui-monospace, monospace); + font-size: 0.55rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--scope-label); + opacity: 0.55; +} + +/* ── CRT bay (recessed dark area) ────────────────────────── */ +.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 ──────────────────────────────────────────── */ +.scope-screen { + position: relative; + background: var(--scope-crt-bg); + border-radius: 3px; + overflow: hidden; + aspect-ratio: 1; + box-shadow: + 0 0 15px var(--scope-teal-glow), + inset 0 1px 4px rgba(0, 0, 0, 0.4); +} + +/* ── Canvas ──────────────────────────────────────────────── */ +.scope-canvas { + display: block; + width: 100%; + height: 100%; +} + +/* ── Graticule overlay (8x10 grid, like the 465) ─────────── */ +.scope-graticule { + position: absolute; + inset: 0; + pointer-events: none; + /* 10 horizontal, 8 vertical — classic Tek grid */ + background-image: + repeating-linear-gradient( + 90deg, + 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) + ), + 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 tick marks */ +.scope-graticule::before, +.scope-graticule::after { + content: ''; + position: absolute; +} + +.scope-graticule::before { + /* horizontal center tick */ + top: 50%; + left: calc(50% - 8px); + width: 16px; + height: 1px; + background: rgba(45, 212, 191, 0.18); +} + +.scope-graticule::after { + /* vertical center tick */ + left: 50%; + top: calc(50% - 8px); + width: 1px; + height: 16px; + background: rgba(45, 212, 191, 0.18); +} + +/* ── Scanline overlay ────────────────────────────────────── */ +.scope-scanlines { + position: absolute; + inset: 0; + pointer-events: none; + 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; +} + +/* ── Control panel area ──────────────────────────────────── */ +.scope-panel { + padding: 8px 12px 6px; + border-top: 1px solid var(--scope-section-line); + margin-top: 10px; +} + +.scope-controls-row { + display: flex; + align-items: flex-start; + gap: 2px; +} + +/* ── Control section (labeled group) ─────────────────────── */ +.scope-section { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 4px 2px; + position: relative; +} + +/* Section divider lines */ +.scope-section + .scope-section::before { + content: ''; + position: absolute; + left: -1px; + top: 0; + bottom: 0; + width: 1px; + background: var(--scope-section-line); +} + +.scope-section-label { + font-family: var(--sl-font-mono, ui-monospace, monospace); + font-size: 0.45rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--scope-label); + opacity: 0.6; + line-height: 1; +} + +/* ── Rotary knob ─────────────────────────────────────────── */ +.scope-knob { + width: 28px; + height: 28px; + border-radius: 50%; + background: radial-gradient(circle at 40% 35%, #3a3a3e, var(--scope-knob)); + border: 2px solid var(--scope-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 */ +.scope-knob::after { + content: ''; + position: absolute; + top: 3px; + left: 50%; + width: 1.5px; + height: 7px; + background: #d4d0c8; + border-radius: 1px; + transform: translateX(-50%); +} + +.scope-knob-label { + font-family: var(--sl-font-mono, ui-monospace, monospace); + font-size: 0.4rem; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--scope-label); + opacity: 0.5; + line-height: 1; +} + +/* ── Power toggle ────────────────────────────────────────── */ +.scope-toggle { + appearance: none; + background: var(--scope-knob); + border: 2px solid var(--scope-knob-ring); + border-radius: 6px; + color: #8a8880; + font-family: var(--sl-font-mono, ui-monospace, monospace); + font-size: 0.55rem; + font-weight: 700; + letter-spacing: 0.06em; + padding: 5px 8px; + cursor: pointer; + transition: color 0.2s, box-shadow 0.2s; + text-transform: uppercase; + line-height: 1; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.scope-toggle:hover { + color: #c4c0b8; +} + +.scope-toggle[data-active="true"] { + color: var(--scope-teal); + box-shadow: + 0 2px 4px rgba(0, 0, 0, 0.3), + 0 0 6px var(--scope-teal-glow); +} + +.scope-toggle:focus-visible { + outline: 2px solid var(--scope-teal); + outline-offset: 2px; +} + +/* ── Power LED ───────────────────────────────────────────── */ +.scope-led { + width: 5px; + height: 5px; + border-radius: 50%; + background: #3a3632; + border: 1px solid rgba(0, 0, 0, 0.3); + transition: background 0.3s, box-shadow 0.3s; +} + +.scope-led[data-on="true"] { + background: var(--scope-teal); + box-shadow: 0 0 6px var(--scope-teal-glow); +} + +/* ── Attribution bar ─────────────────────────────────────── */ +.scope-attribution-bar { + padding: 5px 14px 7px; + border-top: 1px solid var(--scope-section-line); +} + +.scope-attribution { + font-family: var(--sl-font-mono, ui-monospace, monospace); + font-size: 0.45rem; + color: var(--scope-panel-dark); + line-height: 1.4; + text-align: center; +} + +.scope-attribution a { + color: var(--scope-label); + text-decoration: none; + transition: color 0.15s; +} + +.scope-attribution a:hover { + color: #1a1610; +} + +/* ── Idle state ──────────────────────────────────────────── */ +.scope-screen[data-idle="true"] .scope-canvas { + opacity: 0.6; +} + +/* ── Reduced motion ──────────────────────────────────────── */ +@media (prefers-reduced-motion: reduce) { + .scope-scanlines { + display: none; + } +} + +/* ── Responsive ──────────────────────────────────────────── */ +@media (max-width: 50rem) { + .scope-frame { + max-width: 280px; + } + + .scope-knob { + width: 24px; + height: 24px; + } + + .scope-knob::after { + height: 5px; + } +}