From dfb513c12ba93eb82f302902fdecaf03996a7bec Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 24 Apr 2026 23:19:47 -0400 Subject: [PATCH 1/7] Add starfield/neon hero canvas animation --- great_docs/assets/starfield.js | 578 +++++++++++++++++++++++++++++++++ 1 file changed, 578 insertions(+) create mode 100644 great_docs/assets/starfield.js diff --git a/great_docs/assets/starfield.js b/great_docs/assets/starfield.js new file mode 100644 index 0000000..f765a29 --- /dev/null +++ b/great_docs/assets/starfield.js @@ -0,0 +1,578 @@ +/** + * Starfield / Neon-Ring Animation + * + * Dark mode: Parallax starfield with twinkling and quasar glow effects. + * Light mode: Retrofuturist drifting neon ellipses in a vaporwave palette. + * + * Both modes are interactive — moving the pointer steers the focal point. + * Respects prefers-reduced-motion and pauses when offscreen. + */ +(function () { + "use strict"; + + /* ── Shared constants ──────────────────────────────────── */ + const MOUSE_INFLUENCE = 0.12; + + /* ── Dark-mode starfield constants ─────────────────────── */ + const STAR_COUNT = 340; + const SPEED = 0.40; + const DEPTH = 950; + const TWINKLE_SPEED = 0.035; // sine-wave twinkle rate + const QUASAR_CHANCE = 0.07; // fraction of stars that glow + + /* Nebula cloud count */ + const NEBULA_COUNT = 5; + + /* Star colour palette (dark mode) — cool whites, blues, warm hints */ + const STAR_COLORS = [ + [200, 220, 255], // blue-white + [255, 255, 255], // pure white + [180, 200, 255], // periwinkle + [255, 210, 180], // warm amber (rare giant star look) + [210, 180, 255], // soft lavender + ]; + + /* Nebula colour palette — magenta / purple / deep blue */ + const NEBULA_COLORS = [ + [180, 50, 180], // magenta + [140, 40, 200], // deep purple + [100, 60, 220], // violet-blue + [200, 60, 160], // hot magenta + [120, 30, 180], // dark purple + ]; + + /* ── Light-mode neon constants ───────────────────────────── */ + const RING_COUNT = 22; + const ORB_COUNT = 45; + const WIREFRAME_COUNT = 6; + const RING_SPEED = 0.12; + const ORB_SPEED = 0.08; + + /* Vaporwave / retrofuturism palette */ + const NEON_PALETTE = [ + [255, 85, 200], // hot pink + [ 0, 230, 255], // electric cyan + [180, 130, 255], // lavender + [255, 160, 220], // rose + [100, 220, 255], // sky + [200, 100, 255], // purple + [255, 120, 180], // coral-pink + [140, 255, 240], // mint + ]; + + /* ── Canvas setup ──────────────────────────────────────── */ + const canvas = document.getElementById("gd-starfield"); + if (!canvas) return; + const ctx = canvas.getContext("2d"); + + const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + let width, height, cx, cy; + let mouseX = 0, mouseY = 0; + let rafId = null; + let visible = true; + let frame = 0; // global frame counter for animation + + function isDark() { + return document.documentElement.getAttribute("data-bs-theme") === "dark"; + } + + function resize() { + width = window.innerWidth; + height = window.innerHeight; + canvas.width = width * devicePixelRatio; + canvas.height = height * devicePixelRatio; + ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); + cx = width / 2; + cy = height / 2; + } + + /* ── Dark-mode: star pool ──────────────────────────────── */ + var stars = []; + + function createStar(zOverride) { + var color = STAR_COLORS[(Math.random() * STAR_COLORS.length) | 0]; + return { + x: (Math.random() - 0.5) * width * 2, + y: (Math.random() - 0.5) * height * 2, + z: zOverride !== undefined ? zOverride : Math.random() * DEPTH, + phase: Math.random() * Math.PI * 2, // twinkle phase offset + quasar: Math.random() < QUASAR_CHANCE, // whether this star glows + r: color[0], g: color[1], b: color[2], + }; + } + + function initStars() { + stars = []; + for (var i = 0; i < STAR_COUNT; i++) stars.push(createStar()); + } + + /* ── Dark-mode: nebula clouds ──────────────────────────── */ + var nebulae = []; + + function createNebula() { + var c = NEBULA_COLORS[(Math.random() * NEBULA_COLORS.length) | 0]; + return { + x: Math.random() * width, + y: Math.random() * height, + rx: 120 + Math.random() * 250, // horizontal radius + ry: 80 + Math.random() * 160, // vertical radius + angle: Math.random() * Math.PI, // rotation + phase: Math.random() * Math.PI * 2, // pulse phase offset + drift: (Math.random() - 0.5) * 0.06, // slow angular drift + r: c[0], g: c[1], b: c[2], + }; + } + + function initNebulae() { + nebulae = []; + for (var i = 0; i < NEBULA_COUNT; i++) nebulae.push(createNebula()); + } + + function drawNebulae() { + var ox = (mouseX - cx) * MOUSE_INFLUENCE * 0.5; + var oy = (mouseY - cy) * MOUSE_INFLUENCE * 0.5; + + for (var i = 0; i < nebulae.length; i++) { + var n = nebulae[i]; + n.angle += n.drift * 0.003; + // Gentle breathing motion + n.x += Math.sin(frame * 0.003 + n.phase) * 0.15; + n.y += Math.cos(frame * 0.002 + n.phase) * 0.1; + + // Wrap + if (n.x < -300) n.x = width + 300; + if (n.x > width + 300) n.x = -300; + if (n.y < -300) n.y = height + 300; + if (n.y > height + 300) n.y = -300; + + // Pulsate opacity + var pulse = 0.5 + 0.5 * Math.sin(frame * 0.012 + n.phase); + var alpha = 0.025 + pulse * 0.04; + + ctx.save(); + ctx.translate(n.x + ox, n.y + oy); + ctx.rotate(n.angle); + + // Draw as an elliptical radial gradient + // We scale the context to make a circle → ellipse + ctx.scale(1, n.ry / n.rx); + var grad = ctx.createRadialGradient(0, 0, 0, 0, 0, n.rx); + grad.addColorStop(0, "rgba(" + n.r + "," + n.g + "," + n.b + "," + (alpha * 1.5) + ")"); + grad.addColorStop(0.4, "rgba(" + n.r + "," + n.g + "," + n.b + "," + alpha + ")"); + grad.addColorStop(1, "rgba(" + n.r + "," + n.g + "," + n.b + ",0)"); + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.arc(0, 0, n.rx, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + } + } + + function drawStars() { + var ox = cx + (mouseX - cx) * MOUSE_INFLUENCE; + var oy = cy + (mouseY - cy) * MOUSE_INFLUENCE; + + // Draw nebulae first (behind stars) + drawNebulae(); + + for (var i = 0; i < stars.length; i++) { + var s = stars[i]; + s.z -= SPEED; + if (s.z <= 0.5) { stars[i] = createStar(DEPTH); continue; } + + var k = 300 / s.z; + var sx = ox + s.x * k; + var sy = oy + s.y * k; + if (sx < -40 || sx > width + 40 || sy < -40 || sy > height + 40) { + stars[i] = createStar(DEPTH); continue; + } + + var t = 1 - s.z / DEPTH; // 0=far, 1=near + var twinkle = 0.5 + 0.5 * Math.sin(frame * TWINKLE_SPEED + s.phase); + var alpha = (0.15 + t * 0.75) * (0.55 + 0.45 * twinkle); + + // Dramatic size curve: stars get BIG when very close + // Cubic easing makes close stars grow fast + var t3 = t * t * t; + var radius = 0.3 + t * 1.6 + t3 * 8.0; + + // Core dot + ctx.fillStyle = "rgba(" + s.r + "," + s.g + "," + s.b + "," + alpha + ")"; + ctx.beginPath(); + ctx.arc(sx, sy, radius, 0, Math.PI * 2); + ctx.fill(); + + // Quasar / flyby glow on nearby stars (quasars glow earlier, all big stars glow) + var glowThreshold = s.quasar ? 0.3 : 0.7; + if (t > glowThreshold) { + var glowR = radius * (2.5 + t * 5); + var glowA = alpha * 0.22 * t; + var grad = ctx.createRadialGradient(sx, sy, radius * 0.4, sx, sy, glowR); + grad.addColorStop(0, "rgba(" + s.r + "," + s.g + "," + s.b + "," + glowA + ")"); + grad.addColorStop(0.5, "rgba(" + s.r + "," + s.g + "," + s.b + "," + (glowA * 0.3) + ")"); + grad.addColorStop(1, "rgba(" + s.r + "," + s.g + "," + s.b + ",0)"); + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.arc(sx, sy, glowR, 0, Math.PI * 2); + ctx.fill(); + } + } + } + + /* ── Light-mode: neon rings + orbs + wireframes ──────────── */ + var rings = []; + var orbs = []; + var wireframes = []; + + function pickNeon() { return NEON_PALETTE[(Math.random() * NEON_PALETTE.length) | 0]; } + + function createRing() { + var c = pickNeon(); + return { + x: Math.random() * width, + y: Math.random() * height, + rx: 30 + Math.random() * 200, + ry: 15 + Math.random() * 100, + angle: Math.random() * Math.PI, + drift: (Math.random() - 0.5) * RING_SPEED, + phase: Math.random() * Math.PI * 2, + r: c[0], g: c[1], b: c[2], + }; + } + + function createOrb() { + var c = pickNeon(); + // Varied sizes: mostly small, some medium, a few large + var sizeRoll = Math.random(); + var radius = sizeRoll < 0.5 ? 1.5 + Math.random() * 4 + : sizeRoll < 0.85 ? 5 + Math.random() * 10 + : 12 + Math.random() * 20; + return { + x: Math.random() * width, + y: Math.random() * height, + radius: radius, + vx: (Math.random() - 0.5) * ORB_SPEED * (1 + 6 / (radius + 1)), + vy: (Math.random() - 0.5) * ORB_SPEED * (1 + 6 / (radius + 1)), + phase: Math.random() * Math.PI * 2, + r: c[0], g: c[1], b: c[2], + }; + } + + /* 3D wireframe shapes — cube and octahedron vertex definitions */ + var CUBE_VERTS = [ + [-1,-1,-1],[1,-1,-1],[1,1,-1],[-1,1,-1], + [-1,-1, 1],[1,-1, 1],[1,1, 1],[-1,1, 1] + ]; + var CUBE_EDGES = [ + [0,1],[1,2],[2,3],[3,0], + [4,5],[5,6],[6,7],[7,4], + [0,4],[1,5],[2,6],[3,7] + ]; + var OCTA_VERTS = [ + [0,-1,0],[1,0,0],[0,0,1],[-1,0,0],[0,0,-1],[0,1,0] + ]; + var OCTA_EDGES = [ + [0,1],[0,2],[0,3],[0,4], + [5,1],[5,2],[5,3],[5,4], + [1,2],[2,3],[3,4],[4,1] + ]; + + function createWireframe() { + var c = pickNeon(); + var isOcta = Math.random() > 0.5; + return { + x: Math.random() * width, + y: Math.random() * height, + size: 30 + Math.random() * 60, + rotX: Math.random() * Math.PI * 2, + rotY: Math.random() * Math.PI * 2, + rotZ: Math.random() * Math.PI * 2, + spinX: (Math.random() - 0.5) * 0.008, + spinY: (Math.random() - 0.5) * 0.012, + spinZ: (Math.random() - 0.5) * 0.006, + vx: (Math.random() - 0.5) * 0.15, + vy: (Math.random() - 0.5) * 0.1, + phase: Math.random() * Math.PI * 2, + verts: isOcta ? OCTA_VERTS : CUBE_VERTS, + edges: isOcta ? OCTA_EDGES : CUBE_EDGES, + r: c[0], g: c[1], b: c[2], + }; + } + + function initNeon() { + rings = []; + orbs = []; + wireframes = []; + for (var i = 0; i < RING_COUNT; i++) rings.push(createRing()); + for (var i = 0; i < ORB_COUNT; i++) orbs.push(createOrb()); + for (var i = 0; i < WIREFRAME_COUNT; i++) wireframes.push(createWireframe()); + } + + /* Project a 3D point with rotation */ + function project3D(vx, vy, vz, rx, ry, rz) { + // Rotate around X + var cosX = Math.cos(rx), sinX = Math.sin(rx); + var y1 = vy * cosX - vz * sinX; + var z1 = vy * sinX + vz * cosX; + // Rotate around Y + var cosY = Math.cos(ry), sinY = Math.sin(ry); + var x2 = vx * cosY + z1 * sinY; + var z2 = -vx * sinY + z1 * cosY; + // Rotate around Z + var cosZ = Math.cos(rz), sinZ = Math.sin(rz); + var x3 = x2 * cosZ - y1 * sinZ; + var y3 = x2 * sinZ + y1 * cosZ; + return [x3, y3]; + } + + function drawNeon() { + // Top-darkening gradient wash + var gradWash = ctx.createLinearGradient(0, 0, 0, height * 0.5); + gradWash.addColorStop(0, "rgba(40, 20, 60, 0.12)"); + gradWash.addColorStop(0.5, "rgba(30, 15, 50, 0.06)"); + gradWash.addColorStop(1, "rgba(0, 0, 0, 0)"); + ctx.fillStyle = gradWash; + ctx.fillRect(0, 0, width, height); + + // No mouse offset in light mode + + // Elliptical rings + for (var i = 0; i < rings.length; i++) { + var rn = rings[i]; + rn.angle += rn.drift * 0.008; + rn.x += Math.sin(frame * 0.005 + rn.phase) * 0.3; + rn.y += Math.cos(frame * 0.004 + rn.phase) * 0.2; + + if (rn.x < -250) rn.x = width + 250; + if (rn.x > width + 250) rn.x = -250; + if (rn.y < -250) rn.y = height + 250; + if (rn.y > height + 250) rn.y = -250; + + var pulse = 0.5 + 0.5 * Math.sin(frame * 0.02 + rn.phase); + var alpha = 0.10 + pulse * 0.18; + + ctx.save(); + ctx.translate(rn.x, rn.y); + ctx.rotate(rn.angle); + + // Outer glow (wide, soft) + ctx.strokeStyle = "rgba(" + rn.r + "," + rn.g + "," + rn.b + "," + (alpha * 0.3) + ")"; + ctx.lineWidth = 5; + ctx.beginPath(); + ctx.ellipse(0, 0, rn.rx, rn.ry, 0, 0, Math.PI * 2); + ctx.stroke(); + + // Mid glow + ctx.strokeStyle = "rgba(" + rn.r + "," + rn.g + "," + rn.b + "," + (alpha * 0.6) + ")"; + ctx.lineWidth = 2.5; + ctx.beginPath(); + ctx.ellipse(0, 0, rn.rx, rn.ry, 0, 0, Math.PI * 2); + ctx.stroke(); + + // Core ring + ctx.strokeStyle = "rgba(" + rn.r + "," + rn.g + "," + rn.b + "," + alpha + ")"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.ellipse(0, 0, rn.rx, rn.ry, 0, 0, Math.PI * 2); + ctx.stroke(); + + ctx.restore(); + } + + // 3D wireframe shapes + for (var i = 0; i < wireframes.length; i++) { + var w = wireframes[i]; + w.rotX += w.spinX; + w.rotY += w.spinY; + w.rotZ += w.spinZ; + w.x += w.vx + Math.sin(frame * 0.003 + w.phase) * 0.1; + w.y += w.vy + Math.cos(frame * 0.003 + w.phase) * 0.08; + + if (w.x < -150) w.x = width + 150; + if (w.x > width + 150) w.x = -150; + if (w.y < -150) w.y = height + 150; + if (w.y > height + 150) w.y = -150; + + var pulse = 0.5 + 0.5 * Math.sin(frame * 0.018 + w.phase); + var alpha = 0.12 + pulse * 0.20; + + // Project all vertices + var projected = []; + for (var v = 0; v < w.verts.length; v++) { + var pt = w.verts[v]; + var p = project3D(pt[0], pt[1], pt[2], w.rotX, w.rotY, w.rotZ); + projected.push([w.x + p[0] * w.size, w.y + p[1] * w.size]); + } + + // Draw edges with glow + for (var e = 0; e < w.edges.length; e++) { + var a = projected[w.edges[e][0]]; + var b = projected[w.edges[e][1]]; + + // Outer glow + ctx.strokeStyle = "rgba(" + w.r + "," + w.g + "," + w.b + "," + (alpha * 0.25) + ")"; + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.moveTo(a[0], a[1]); + ctx.lineTo(b[0], b[1]); + ctx.stroke(); + + // Core edge + ctx.strokeStyle = "rgba(" + w.r + "," + w.g + "," + w.b + "," + alpha + ")"; + ctx.lineWidth = 1.2; + ctx.beginPath(); + ctx.moveTo(a[0], a[1]); + ctx.lineTo(b[0], b[1]); + ctx.stroke(); + } + + // Vertex dots with glow + for (var v = 0; v < projected.length; v++) { + var px = projected[v][0], py = projected[v][1]; + var dotR = 2 + pulse * 1.5; + var gRad = dotR * 4; + var grd = ctx.createRadialGradient(px, py, dotR * 0.3, px, py, gRad); + grd.addColorStop(0, "rgba(" + w.r + "," + w.g + "," + w.b + "," + (alpha * 0.7) + ")"); + grd.addColorStop(1, "rgba(" + w.r + "," + w.g + "," + w.b + ",0)"); + ctx.fillStyle = grd; + ctx.beginPath(); + ctx.arc(px, py, gRad, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = "rgba(" + w.r + "," + w.g + "," + w.b + "," + alpha + ")"; + ctx.beginPath(); + ctx.arc(px, py, dotR, 0, Math.PI * 2); + ctx.fill(); + } + } + + // Floating orbs (varied sizes, stronger glow) + for (var i = 0; i < orbs.length; i++) { + var o = orbs[i]; + o.x += o.vx + Math.sin(frame * 0.01 + o.phase) * 0.15; + o.y += o.vy + Math.cos(frame * 0.01 + o.phase) * 0.1; + + var margin = o.radius * 4; + if (o.x < -margin) o.x = width + margin; + if (o.x > width + margin) o.x = -margin; + if (o.y < -margin) o.y = height + margin; + if (o.y > height + margin) o.y = -margin; + + var pulse = 0.5 + 0.5 * Math.sin(frame * 0.025 + o.phase); + var alpha = 0.12 + pulse * 0.28; + var glowR = o.radius * (3 + pulse * 4); + + // Outer glow + var grad = ctx.createRadialGradient( + o.x, o.y, o.radius * 0.2, + o.x, o.y, glowR + ); + grad.addColorStop(0, "rgba(" + o.r + "," + o.g + "," + o.b + "," + (alpha * 0.7) + ")"); + grad.addColorStop(0.4, "rgba(" + o.r + "," + o.g + "," + o.b + "," + (alpha * 0.3) + ")"); + grad.addColorStop(1, "rgba(" + o.r + "," + o.g + "," + o.b + ",0)"); + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.arc(o.x, o.y, glowR, 0, Math.PI * 2); + ctx.fill(); + + // Core + ctx.fillStyle = "rgba(" + o.r + "," + o.g + "," + o.b + "," + (alpha * 1.2) + ")"; + ctx.beginPath(); + ctx.arc(o.x, o.y, o.radius, 0, Math.PI * 2); + ctx.fill(); + } + } + + /* ── Main loop ─────────────────────────────────────────── */ + function draw() { + ctx.clearRect(0, 0, width, height); + frame++; + if (isDark()) { + drawStars(); + } else { + drawNeon(); + } + } + + function loop() { + if (!visible) return; + draw(); + rafId = requestAnimationFrame(loop); + } + + function onPointerMove(e) { + // Only track pointer in dark mode (starfield steering) + if (!isDark()) return; + mouseX = e.clientX; + mouseY = e.clientY; + } + + /* ── Scroll-based fade ──────────────────────────────────── */ + // Instead of hard-stopping the animation when the hero scrolls away, + // smoothly fade the canvas opacity to 0 as the hero leaves the viewport. + var heroEl = canvas.parentElement; + + function updateScrollFade() { + if (!heroEl) return; + var heroRect = heroEl.getBoundingClientRect(); + var heroBottom = heroRect.bottom; + var fadeZone = heroRect.height * 0.6; + + if (heroBottom > heroRect.height) { + // Hero fully in view + canvas.style.opacity = "1"; + if (!visible) { visible = true; if (!rafId) loop(); } + } else if (heroBottom > -fadeZone) { + // Fading zone: hero partially scrolled out + var t = Math.max(0, heroBottom + fadeZone) / (heroRect.height + fadeZone); + canvas.style.opacity = String(Math.max(0, t)); + if (!visible) { visible = true; if (!rafId) loop(); } + } else { + // Fully scrolled past — stop to save resources + canvas.style.opacity = "0"; + visible = false; + if (rafId) { cancelAnimationFrame(rafId); rafId = null; } + } + } + + window.addEventListener("scroll", updateScrollFade, { passive: true }); + + /* ── Initialization ────────────────────────────────────── */ + resize(); + initStars(); + initNebulae(); + initNeon(); + + updateScrollFade(); + + if (!reducedMotion) { + document.addEventListener("pointermove", onPointerMove); + mouseX = cx; + mouseY = cy; + loop(); + } else { + draw(); + } + + var resizeTimer; + window.addEventListener("resize", function () { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(function () { + resize(); + // Re-scatter elements to fill new dimensions + initNebulae(); + initNeon(); + }, 100); + }); + + // Re-init when theme toggles so the right mode is ready + var themeObserver = new MutationObserver(function () { + if (reducedMotion) draw(); + }); + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-bs-theme"], + }); +})(); From 2b7d5f2ef38ec1e325077ba585eb20f579433894 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 24 Apr 2026 23:20:43 -0400 Subject: [PATCH 2/7] Add homepage-specific improvements --- great_docs/assets/great-docs.scss | 265 ++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) diff --git a/great_docs/assets/great-docs.scss b/great_docs/assets/great-docs.scss index 4091dba..1e45d07 100644 --- a/great_docs/assets/great-docs.scss +++ b/great_docs/assets/great-docs.scss @@ -3963,13 +3963,67 @@ html:not(.theme-loaded) *::after { body[data-page="index"] .column-margin { grid-column: body-end / page-end !important; } + + /* In dark mode every container gets an opaque --gd-bg-primary background + that hides the fixed starfield canvas (z-index: -1). On the homepage + we punch all those layers transparent so the canvas shows through. + body/html keep their backgrounds — they sit BELOW the canvas in the + root stacking context and provide the base dark colour where the + canvas mask fades out. */ + body.gd-homepage.quarto-dark #quarto-content, + body.gd-homepage.quarto-dark main.content, + body.gd-homepage.quarto-dark #quarto-document-content, + body.gd-homepage.quarto-dark .quarto-body-content, + body.gd-homepage.quarto-dark .page-columns, + body.gd-homepage.quarto-dark .page-rows-contents, + body.gd-homepage.quarto-dark .column-body, + body.gd-homepage.quarto-dark article, + body.gd-homepage.quarto-dark section { + background-color: transparent; + } } /* ── Hero Section ─────────────────────────────────────── */ +/* Prevent horizontal scrollbar from the 100vw starfield canvas */ +.gd-homepage { + overflow-x: clip; +} + .gd-hero { text-align: center; padding: 2.5rem 1rem 2rem; + position: relative; + overflow: visible; +} + +/* Starfield canvas: breaks out of the hero to cover the full viewport width + and extends above into the navbar area and below into body content. + Uses fixed positioning relative to the viewport so Quarto's grid layout + doesn't clip or offset it. */ +#gd-starfield { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; /* let clicks pass through to page content */ + z-index: 0; + transition: opacity 0.15s ease-out; + mask-image: linear-gradient(to bottom, black 35%, transparent 85%); + -webkit-mask-image: linear-gradient(to bottom, black 35%, transparent 85%); +} + +/* Re-enable pointer events only on the homepage hero area so the + dark-mode starfield can track the cursor there. */ +.gd-hero #gd-starfield { + pointer-events: auto; +} + +/* Everything in the hero except the canvas must sit above it */ +.gd-hero > *:not(#gd-starfield) { + position: relative; + z-index: 1; } /* When a column-margin sidebar is present, Quarto adds page-full to
@@ -4047,6 +4101,217 @@ html[data-bs-theme="dark"] .navbar-logo.dark-content { display: inline !importa margin-top: 0.5rem !important; } +/* ── CTA Cards ("Go Further" section) ─────────────────── */ + +.gd-cta-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.25rem; + margin: 1.5rem 0 2rem; +} + +.gd-cta-card { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 1.5rem; + border-radius: 12px; + border: 1px solid rgba(0, 0, 0, 0.08); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(248, 249, 250, 0.7)); + text-decoration: none !important; + color: inherit !important; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + position: relative; + overflow: hidden; + + &:hover { + transform: translateY(-3px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); + border-color: rgba(0, 0, 0, 0.15); + } + + &:hover .gd-cta-arrow { + transform: translateX(4px); + opacity: 1; + } +} + +.gd-cta-icon { + font-size: 2rem; + margin-bottom: 0.75rem; + line-height: 1; +} + +.gd-cta-title { + font-size: 1.15rem; + font-weight: 600; + margin-bottom: 0.4rem; + line-height: 1.3; +} + +.gd-cta-desc { + font-size: 0.9rem; + line-height: 1.5; + color: #6c757d; + flex-grow: 1; +} + +.gd-cta-arrow { + font-size: 1.25rem; + margin-top: 0.75rem; + opacity: 0.4; + transition: transform 0.2s ease, opacity 0.2s ease; + font-weight: 600; +} + +/* Dark mode CTA cards */ +body.quarto-dark .gd-cta-card { + border-color: rgba(255, 255, 255, 0.1); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02)); + + &:hover { + border-color: rgba(255, 255, 255, 0.2); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); + } +} + +body.quarto-dark .gd-cta-desc { + color: #adb5bd; +} + +@media (max-width: 768px) { + .gd-cta-grid { + grid-template-columns: 1fr; + gap: 1rem; + } +} + +/* ── Feature Wall ("And So Much More") ────────────────── */ + +.gd-feature-wall { + margin: 3rem 0 2rem; + hyphens: auto; + -webkit-hyphens: auto; + overflow-wrap: break-word; +} + +.gd-fw-heading { + text-align: center; + font-weight: 700; + margin-bottom: 1.5rem; + letter-spacing: -0.01em; +} + +.gd-fw-large, +.gd-fw-medium, +.gd-fw-small { + text-align: justify; + text-justify: inter-word; +} + +.gd-fw-large { + font-size: 1.65rem; + line-height: 1.45; + margin-bottom: 0.6rem; + letter-spacing: -0.01em; +} + +.gd-fw-medium { + font-size: 1.15rem; + line-height: 1.55; + margin-bottom: 0.5rem; +} + +.gd-fw-small { + font-size: 0.88rem; + line-height: 1.65; +} + +/* Dim the middle-dot separators slightly */ +.gd-feature-wall b { + font-weight: 650; +} + +@media (max-width: 768px) { + .gd-fw-large { + font-size: 1.35rem; + } + .gd-fw-medium { + font-size: 1.05rem; + } + .gd-fw-small { + font-size: 0.82rem; + } +} + +/* ── Feature Wall Closing ─────────────────────────────── */ + +.gd-fw-rule { + border: none; + height: 2px; + margin: 3rem auto 2rem; + max-width: 30rem; + background: linear-gradient( + 90deg, + transparent, + var(--gd-accent, #6366f1) 25%, + var(--gd-accent-secondary, #a855f7) 50%, + var(--gd-accent, #6366f1) 75%, + transparent + ); + background-size: 200% 100%; + animation: gd-rule-shimmer 4s ease-in-out infinite; + opacity: 0.7; +} + +@keyframes gd-rule-shimmer { + 0% { background-position: 100% 0; } + 50% { background-position: 0% 0; } + 100% { background-position: 100% 0; } +} + +.gd-fw-closing { + text-align: center; + padding: 0 1.5rem 2.5rem; + max-width: 38rem; + margin: 0 auto; + position: relative; + z-index: 1; +} + +.gd-fw-closing p { + font-size: 1.05rem; + line-height: 1.7; + color: #6b7280; + margin: 0; +} + +body.quarto-dark .gd-fw-closing p { + color: #9ca3af; +} + +.gd-fw-closing a { + color: var(--gd-accent, #6366f1); + text-decoration: none; + font-weight: 600; + border-bottom: 1px solid transparent; + transition: border-color 0.2s ease; +} + +.gd-fw-closing a:hover { + border-bottom-color: var(--gd-accent, #6366f1); +} + +@media (max-width: 768px) { + .gd-fw-rule { + max-width: 80%; + margin: 2rem auto 1.5rem; + } + .gd-fw-closing p { + font-size: 0.95rem; + } +} + /* ── Hero Responsive ──────────────────────────────────── */ @media (max-width: 768px) { From cf9101ee4e20b978bf4c0a02a8cd4824e8ad42e0 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 24 Apr 2026 23:20:46 -0400 Subject: [PATCH 3/7] Update great-docs.yml --- great-docs.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/great-docs.yml b/great-docs.yml index b8731be..8a349f8 100644 --- a/great-docs.yml +++ b/great-docs.yml @@ -39,6 +39,14 @@ logo: favicon: assets/favicon.svg +# Hero Section +# ------------ +# The large header area on the homepage with logo, name, and tagline. +hero: + enabled: true + tagline: "Documentation sites for Python packages. Three commands. Zero friction." + starfield: true + # Versioned Documentation # ----------------------- # Publish multiple versions of the site from a single source tree. From 3b6daae38f0c987d5d44dc349e6e53abc33c0e8c Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 24 Apr 2026 23:20:59 -0400 Subject: [PATCH 4/7] Add hero_starfield config property --- great_docs/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/great_docs/config.py b/great_docs/config.py index c1dfaea..329e91c 100644 --- a/great_docs/config.py +++ b/great_docs/config.py @@ -974,6 +974,12 @@ def hero_tagline(self) -> str | None: return None return val + @property + def hero_starfield(self) -> bool: + """Whether the interactive starfield animation is enabled on the hero.""" + hero = self.hero + return bool(hero.get("starfield", False)) if hero else False + @property def hero_badges(self) -> str | list | None: """Get the hero badges config. From ff07c6e4c6e58c96379342dbfc28182507da396a Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 24 Apr 2026 23:21:08 -0400 Subject: [PATCH 5/7] Add optional hero starfield support --- great_docs/core.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/great_docs/core.py b/great_docs/core.py index b5da846..9d9ad14 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -228,6 +228,8 @@ def _prepare_build_directory(self) -> None: js_files.append("page-status-badges.js") # pragma: no cover if self._config.has_versions: js_files.append("version-selector.js") + if self._config.hero_starfield: + js_files.append("starfield.js") for js_file in js_files: js_src = self.assets_path / js_file if js_src.exists(): @@ -8598,7 +8600,17 @@ def _build_hero_section( if not parts: return "", None # pragma: no cover - hero_html = '
\n' + "\n".join(parts) + "\n
\n\n" + # ── Starfield canvas (optional) ───────────────────────────── + starfield_html = "" + if self._config.hero_starfield: + starfield_html = '\n' + + hero_html = '
\n' + starfield_html + "\n".join(parts) + "\n
\n\n" + + # Append starfield script tag after the hero div so the canvas + # is in the DOM when the script executes. + if self._config.hero_starfield: + hero_html += '\n\n' return hero_html, cleaned_content @@ -9830,6 +9842,8 @@ def _update_quarto_config(self) -> None: js_resource_files.append("page-tags.js") # pragma: no cover if self._config.page_status_enabled: js_resource_files.append("page-status-badges.js") # pragma: no cover + if self._config.hero_starfield: + js_resource_files.append("starfield.js") for js_file in js_resource_files: if js_file not in config["project"]["resources"]: config["project"]["resources"].append(js_file) From 59bbe58d7b9c1b0ef33097d5263408a6dca944b3 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 24 Apr 2026 23:21:12 -0400 Subject: [PATCH 6/7] Update index.qmd --- index.qmd | 96 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/index.qmd b/index.qmd index 9a1b1c3..8dca00b 100644 --- a/index.qmd +++ b/index.qmd @@ -1,65 +1,81 @@ # Great Docs -A documentation site generator for Python packages. Run `great-docs init` in your project directory and get a complete documentation site with auto-generated API reference, CLI docs, and modern styling. +Your Python package deserves a website that looks like you actually care about it. Great Docs turns your docstrings, README, and CLI into a polished documentation site. No templates to wrangle, no config files to debug, it's really great. -## Install +## Three Commands. One Beautiful Site. ```bash pip install great-docs -``` - -## Quick Start -```bash cd your-python-package - -great-docs init # detect your package and generate config -great-docs build # generate and render the site -great-docs preview # view at localhost:3000 +great-docs init # scans your package, writes the config +great-docs build # generates everything, renders with Quarto +great-docs preview # live preview at localhost:3000 ``` -## What It Does +That's it. Your API reference, user guide, CLI docs, and landing page: all generated, all styled, all linked. Push to GitHub and you're live. + +## Your Docstrings, Finally Doing Their Job -Great Docs inspects your Python package to generate a full documentation site: +Every class, function, method, and attribute in your public API gets its own structured page. Parameter tables, return types, source links, and rendered examples. Numpy, Google, and Sphinx docstring styles all work out of the box. -- **API Reference**: classes, functions, methods, and attributes organized into structured pages with parameter tables, return types, examples, and source links -- **CLI Reference**: Click-based CLIs documented automatically with terminal-style help output -- **Landing Page**: your README transformed into a homepage with a metadata sidebar -- **User Guide**: narrative documentation from your `user_guide/` directory -- **Custom Sections**: recipes, blog posts, tutorials, or any other content you add +If your package has a Click CLI, that's documented as well (complete with terminal-style help output and subcommand trees). -Everything is rendered by [Quarto](https://quarto.org/) into a static site you can host anywhere. +## Looks Good Without Trying -## Site Features +Dark mode with a persistent toggle. Animated gradient navbars in eight presets. GitHub star counts in the corner. Sidebar search for large APIs. Responsive on every screen. Announcement banners when you ship something big. -- Dark mode toggle with persistent preference -- Gradient navbar themes (8 presets) -- GitHub widget with live star and fork counts -- Sidebar search filter for large APIs -- Source links to GitHub for every documented item -- Responsive layout for mobile and desktop -- Announcement banners (dismissible, styled) -- `llms.txt` and `llms-full.txt` for AI documentation indexing -- Agent Skills generation ([agentskills.io](https://agentskills.io/) compliant) +You get a site that looks like a funded startup's product docs, except it was built by a single `great-docs build`. -## Configuration +## AI-Native from Day One -All options live in `great-docs.yml`. The `init` command generates this for you. See the [Configuration Guide](user-guide/configuration.qmd) for the full reference. +Great Docs generates `llms.txt` and `llms-full.txt` so AI coding agents can understand your library without scraping HTML. It also produces Agent Skills files: structured metadata that tells tools like Copilot, Cursor, and Claude Code *how* to use your package (not just what it exports). -## Deploy to GitHub Pages +## Deploy in One Step ```bash great-docs setup-github-pages ``` -Creates a GitHub Actions workflow that builds and publishes your site on every push to `main`. - -## Learn More - -- [User Guide](user-guide/installation.qmd) — setup, configuration, theming, and deployment -- [Recipes](recipes/hide-internal-symbols.qmd) — step-by-step guides for common tasks -- [API Reference](reference/index.qmd) — Great Docs' own API documentation +Creates a GitHub Actions workflow. Every push to `main` rebuilds and publishes your site automatically. + +## Go Further + +```{=html} + +``` -## License +```{=html} +
+

And So Much More.

+

Auto-Generated API Ref­er­ence • Dark Mode Toggle • llms.txt & Agent Skills • Multi-Version Docu­men­ta­tion • GitHub Pages in One Com­mand • Click CLI Docu­men­ta­tion • Animated Gradient Nav­bars & Ban­ners • Three Com­mands to Launch • Change­log from GitHub Re­leases • Hero Section with Star­field • Rich Table Pre­views for Pandas, Polars & Arrow

+

Numpy, Google & Sphinx Doc­strings • Version Selector Widget • Side­bar Search & Fil­ter­ing • Page Tags & Status Badges • Mer­maid Dia­gram Ren­der­ing • Inter­active Table Ex­plor­er • Docu­men­ta­tion Linter • Spelling & Grammar Check­ing • Twenty-Three Lan­guages • Key­board Navi­ga­tion • Custom Sections & Recipes • Respon­sive on Every Screen • GitHub Star Count Widget • SEO Audit & Site­map Gen­er­a­tion • Announce­ment Ban­ners • Auto-Detected Logos with Dark-Mode Vari­ants • Copy & View Page as Mark­down • API Diff Between Ver­sions • Tool­tips & Pop­overs • Rich Meta­data Side­bar with De­vel­oper Links • Code Auto­links in Doc­strings • Sub­mod­ule Intro­spec­tion

+

APCA Contrast Algo­rithm • Fav­icon Auto-Gen­er­a­tion • Scale-to-Fit Wide Tables • Copy Code Buttons • Syn­tax High­light­ing & Exe­cut­able Code Blocks • Version Fences & Badges • Breaking Change Detec­tion • API Sur­face Time­line • Param­eter Evo­lu­tion Tables • JSON-LD Struc­tured Data • Canon­ical URLs • Social Cards & Open Graph • Source Links to GitHub • 1,941 Bundled Lucide Icons • Commu­nity File Pages • SPDX License Data­base • PR Pre­view Deploy­ments • Custom SCSS Theming • Blog Sup­port • Cross-Refs, %see­also & In­line Inter­links • Video Embeds • Color Swatches • Tab­sets & Cal­louts • Link Checker • Config Gen­er­a­tion • Robots.txt & Crawl Rules • Non-Breaking Re­build Mode • API Snap­shots to JSON • Custom Static Pages • Object-Type Label Colors • Styled Sig­na­tures & Type Anno­ta­tions • Meth­od Mem­ber Con­trol & Ex­clude Lists • Nav­bar & Side­bar Icons • In­line Icons • Back-to-Top Button • Verbose Build Log­ging • Custom Head In­jec­tion for Scripts & Meta • And Yes, It Builds Fast

+
+``` -MIT. See [LICENSE](license.qmd) for the full text. +```{=html} +
+
+

Something missing? Open an issue (the wall o' text above is not done yet).

+
+``` From e2e8abe7ce74ddda220ba6accc68fe122cc48f7d Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 24 Apr 2026 23:22:53 -0400 Subject: [PATCH 7/7] Update index.qmd --- index.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.qmd b/index.qmd index 8dca00b..82a1051 100644 --- a/index.qmd +++ b/index.qmd @@ -25,7 +25,7 @@ If your package has a Click CLI, that's documented as well (complete with termin Dark mode with a persistent toggle. Animated gradient navbars in eight presets. GitHub star counts in the corner. Sidebar search for large APIs. Responsive on every screen. Announcement banners when you ship something big. -You get a site that looks like a funded startup's product docs, except it was built by a single `great-docs build`. +Your package gets a site that matches the quality of the code behind it. ## AI-Native from Day One