From 6b77213f91578e793ec65a773dd93fe2f3d49971 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sun, 11 Jan 2026 14:37:46 -0800 Subject: [PATCH] Redesign hero, add theme toggle, fix dark mode - Remove GPU-intensive shader from hero section - Combine hero with problem section for immediate impact - Add before/after demo directly in first viewport - Move "works with" logos inline with CTA - Remove redundant "Common AI defaults" tags - Add three-way theme toggle (light/system/dark) - Persist theme preference to localStorage - Position toggle in bottom-right corner - Fix dark mode throughout: - Update glass terminal and source window backgrounds - Add CSS custom properties for framework category colors - Muted dark backgrounds with brighter text for cards - Replace smooth scroll with instant jump for anchor links - Remove Lenis smooth scroll library dependency Co-Authored-By: Claude Opus 4.5 --- public/app.js | 9 +- public/css/main.css | 502 +++++++++++++------------- public/css/workflow.css | 19 +- public/index.html | 137 +++---- public/js/components/framework-viz.js | 13 +- public/js/utils/scroll.js | 70 +--- public/js/utils/theme.js | 56 +++ 7 files changed, 376 insertions(+), 430 deletions(-) create mode 100644 public/js/utils/theme.js diff --git a/public/app.js b/public/app.js index e867935d5..042f46625 100644 --- a/public/app.js +++ b/public/app.js @@ -4,9 +4,9 @@ import { } from "./js/components/glass-terminal.js"; import { initLensEffect } from "./js/components/lens.js"; import { initFrameworkViz } from "./js/components/framework-viz.js"; -import { initHeroShader } from "./js/effects/hero-shader.js"; import { initScrollReveal } from "./js/utils/reveal.js"; -import { initScrollIndicator, initSmoothScroll, initHashTracking } from "./js/utils/scroll.js"; +import { initAnchorScroll, initHashTracking } from "./js/utils/scroll.js"; +import { initThemeToggle } from "./js/utils/theme.js"; // ============================================ // STATE @@ -315,10 +315,9 @@ function initLightbox() { // ============================================ function init() { - initSmoothScroll(); - initScrollIndicator(); + initThemeToggle(); + initAnchorScroll(); initHashTracking(); - initHeroShader(); initLensEffect(); initScrollReveal(); initGlassTerminal(); diff --git a/public/css/main.css b/public/css/main.css index 9a3494470..b50875e07 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -61,11 +61,78 @@ --color-accent-hover: oklch(52% 0.25 350); --color-accent-dim: oklch(60% 0.25 350 / 0.15); --color-accent-soft: oklch(60% 0.25 350 / 0.25); + + /* Framework category colors - Light mode */ + --cat-diagnostic-bg: #fdf4ff; + --cat-diagnostic-border: #d946ef; + --cat-diagnostic-text: #a21caf; + + --cat-quality-bg: #f0fdf4; + --cat-quality-border: #22c55e; + --cat-quality-text: #15803d; + + --cat-intensity-bg: #fffbeb; + --cat-intensity-border: #f59e0b; + --cat-intensity-text: #b45309; + + --cat-adaptation-bg: #eff6ff; + --cat-adaptation-border: #3b82f6; + --cat-adaptation-text: #1d4ed8; + + --cat-enhancement-bg: #fdf2f8; + --cat-enhancement-border: #ec4899; + --cat-enhancement-text: #be185d; + + --cat-system-bg: #f5f5f4; + --cat-system-border: #78716c; + --cat-system-text: #44403c; } -/* Dark Mode */ +/* Dark Mode - Class-based for toggle support */ +html.dark { + --color-ink: oklch(98% 0 0); + --color-text: oklch(98% 0 0); + --color-paper: oklch(10% 0 0); + --color-cream: oklch(12% 0.005 350); + --color-charcoal: oklch(92% 0 0); + --color-ash: oklch(65% 0 0); + --color-mist: oklch(20% 0 0); + --color-bg: oklch(12% 0.005 350); + + --color-accent: oklch(68% 0.23 350); + --color-accent-hover: oklch(76% 0.23 350); + --color-accent-dim: oklch(68% 0.23 350 / 0.15); + --color-accent-soft: oklch(68% 0.23 350 / 0.25); + + /* Framework category colors - Dark mode (muted backgrounds, brighter accents) */ + --cat-diagnostic-bg: #2d1f3d; + --cat-diagnostic-border: #c084fc; + --cat-diagnostic-text: #e879f9; + + --cat-quality-bg: #14332a; + --cat-quality-border: #4ade80; + --cat-quality-text: #86efac; + + --cat-intensity-bg: #362a1a; + --cat-intensity-border: #fbbf24; + --cat-intensity-text: #fcd34d; + + --cat-adaptation-bg: #1e293b; + --cat-adaptation-border: #60a5fa; + --cat-adaptation-text: #93c5fd; + + --cat-enhancement-bg: #3b1f32; + --cat-enhancement-border: #f472b6; + --cat-enhancement-text: #f9a8d4; + + --cat-system-bg: #1c1917; + --cat-system-border: #a8a29e; + --cat-system-text: #d6d3d1; +} + +/* System preference fallback (when no explicit preference is set) */ @media (prefers-color-scheme: dark) { - :root { + html:not(.light):not(.dark) { --color-ink: oklch(98% 0 0); --color-text: oklch(98% 0 0); --color-paper: oklch(10% 0 0); @@ -74,14 +141,84 @@ --color-ash: oklch(65% 0 0); --color-mist: oklch(20% 0 0); --color-bg: oklch(12% 0.005 350); - + --color-accent: oklch(68% 0.23 350); --color-accent-hover: oklch(76% 0.23 350); --color-accent-dim: oklch(68% 0.23 350 / 0.15); --color-accent-soft: oklch(68% 0.23 350 / 0.25); + + /* Framework category colors - Dark mode */ + --cat-diagnostic-bg: #2d1f3d; + --cat-diagnostic-border: #c084fc; + --cat-diagnostic-text: #e879f9; + + --cat-quality-bg: #14332a; + --cat-quality-border: #4ade80; + --cat-quality-text: #86efac; + + --cat-intensity-bg: #362a1a; + --cat-intensity-border: #fbbf24; + --cat-intensity-text: #fcd34d; + + --cat-adaptation-bg: #1e293b; + --cat-adaptation-border: #60a5fa; + --cat-adaptation-text: #93c5fd; + + --cat-enhancement-bg: #3b1f32; + --cat-enhancement-border: #f472b6; + --cat-enhancement-text: #f9a8d4; + + --cat-system-bg: #1c1917; + --cat-system-border: #a8a29e; + --cat-system-text: #d6d3d1; } } +/* Theme Toggle */ +.theme-toggle { + position: fixed; + bottom: var(--spacing-md); + right: var(--spacing-md); + z-index: 10001; + display: flex; + align-items: center; + background: var(--color-cream); + border: 1px solid var(--color-mist); + border-radius: 100px; + padding: 4px; + gap: 2px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.theme-toggle-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + background: transparent; + border-radius: 50%; + color: var(--color-ash); + cursor: pointer; + transition: all 0.2s ease; +} + +.theme-toggle-btn:hover { + color: var(--color-charcoal); + background: var(--color-mist); +} + +.theme-toggle-btn.active { + color: var(--color-paper); + background: var(--color-ink); +} + +.theme-toggle-btn svg { + width: 16px; + height: 16px; +} + /* ============================================ SKIP LINK (Accessibility) ============================================ */ @@ -119,23 +256,6 @@ html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; - scroll-behavior: smooth; -} - -html.lenis, html.lenis body { - height: auto; -} - -.lenis.lenis-smooth { - scroll-behavior: auto !important; -} - -.lenis.lenis-smooth [data-lenis-prevent] { - overscroll-behavior: contain; -} - -.lenis.lenis-stopped { - overflow: hidden; } body { @@ -278,54 +398,52 @@ code { } /* ============================================ - HERO SECTION + HERO SECTION - Combined Layout ============================================ */ -/* Hero shader viewport - first thing, full screen */ -.hero-shader { - width: 100vw; - height: 100vh; - height: 100dvh; - position: relative; -} - -.hero-shader canvas { - width: 100% !important; - height: 100% !important; -} - -/* Overlay content centered on shader */ -.hero-overlay-content { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 2; +.hero-combined { + min-height: 100vh; + min-height: 100dvh; display: flex; flex-direction: column; + justify-content: center; + padding: var(--spacing-xl) var(--spacing-lg); + background: var(--color-paper); +} + +.hero-combined-container { + max-width: var(--width-max); + margin: 0 auto; + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-xl); align-items: center; + width: 100%; +} + +@media (max-width: 1024px) { + .hero-combined-container { + grid-template-columns: 1fr; + gap: var(--spacing-lg); + text-align: center; + } +} + +.hero-combined-left { + display: flex; + flex-direction: column; gap: var(--spacing-md); - text-align: center; - pointer-events: none; } -.hero-badge { - display: inline-block; - padding: 0.35em 1em; - font-family: var(--font-body); - font-size: 0.75rem; - font-weight: 500; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--color-ink); - background: rgba(255, 255, 255, 0.85); - border: 1px solid var(--color-ink); - backdrop-filter: blur(4px); +@media (max-width: 1024px) { + .hero-combined-left { + align-items: center; + } } -.hero-title-overlay { +.hero-title-combined { font-family: var(--font-display); - font-size: clamp(3.5rem, 12vw, 8rem); + font-size: clamp(3.5rem, 10vw, 7rem); font-weight: 300; font-style: italic; line-height: 1; @@ -334,22 +452,40 @@ code { color: var(--color-ink); } -.hero-tagline-overlay { +.hero-tagline-combined { font-family: var(--font-display); - font-size: clamp(1rem, 2.5vw, 1.5rem); + font-size: clamp(1.125rem, 2.5vw, 1.75rem); font-weight: 400; font-style: italic; - line-height: 1.4; + line-height: 1.3; margin: 0; - color: var(--color-ink); - opacity: 0.8; - max-width: 400px; + color: var(--color-charcoal); } -.hero-cta-overlay { - pointer-events: auto; +.hero-problem-text { + font-size: clamp(1rem, 1.5vw, 1.125rem); + line-height: 1.6; + color: var(--color-ash); + max-width: 45ch; + margin: 0; +} + +.hero-cta-group { + display: flex; + align-items: center; + gap: var(--spacing-lg); + margin-top: var(--spacing-sm); +} + +@media (max-width: 600px) { + .hero-cta-group { + flex-direction: column; + gap: var(--spacing-md); + } +} + +.hero-cta-combined { display: inline-block; - margin-top: var(--spacing-md); padding: var(--spacing-sm) var(--spacing-xl); font-family: var(--font-body); font-size: 0.9rem; @@ -360,225 +496,71 @@ code { color: var(--color-paper); background: var(--color-ink); border: none; - transition: transform 0.2s ease, opacity 0.2s ease; + transition: transform 0.2s ease, background 0.2s ease; } -.hero-cta-overlay:hover { +.hero-cta-combined:hover { transform: translateY(-2px); - opacity: 0.9; + background: var(--color-accent); + color: var(--color-paper); } -/* Content section below shader */ -.hero-container { - position: relative; - text-align: center; - max-width: 1000px; - width: 100%; - margin: 0 auto; - padding: var(--spacing-2xl) var(--spacing-lg); - background: var(--color-paper); -} - -.hero-section-title { - font-family: var(--font-display); - font-size: clamp(1.5rem, 4vw, 2rem); - font-weight: 400; - font-style: italic; - color: var(--color-ink); - margin: 0 0 var(--spacing-lg) 0; -} - -.hero-content { +.hero-logos-inline { display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--spacing-xl); - height: 100%; -} - -.hero-header { - text-align: center; -} - -.hero-footer { - display: flex; - flex-direction: column; align-items: center; gap: var(--spacing-sm); } -.hero-eyebrow { - font-family: var(--font-mono); - font-size: 0.8125rem; - font-weight: 500; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--color-accent); - /* Animation handled by JS reveal.js */ -} - -.hero-title { - font-size: clamp(3.5rem, 12vw, 8rem); - font-weight: 300; - font-style: italic; - line-height: 1; - letter-spacing: -0.02em; - margin: 0; - color: var(--color-ink); -} - -.hero-title-line { - display: block; -} - -.hero-title-accent { - font-style: italic; - color: var(--color-accent); -} - -.hero-subtitle { - font-size: clamp(1rem, 2.5vw, 1.375rem); - color: var(--color-charcoal); - max-width: 45ch; - line-height: 1.5; -} - - -.hero-providers { - font-size: 0.8125rem; +.hero-logos-inline .hero-logos-label { + font-size: 0.6875rem; color: var(--color-ash); letter-spacing: 0.03em; } -.hero-tagline { - font-family: var(--font-display); - font-size: clamp(1rem, 1.8vw, 1.25rem); - font-style: italic; - color: var(--color-ash); - letter-spacing: 0.02em; - margin-top: var(--spacing-sm); -} - -/* Hero Equation */ -.hero-equation { - display: flex; - align-items: stretch; - gap: var(--spacing-md); - width: 100%; -} - -.equation-block { - flex: 1; - padding: var(--spacing-sm) var(--spacing-md); - border: 2px solid var(--color-ink); - border-radius: 0; - background: var(--color-cream); - text-align: left; -} - -.equation-label { - display: inline-block; - font-family: var(--font-body); - font-size: 0.625rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.12em; - color: var(--color-cream); - background: var(--color-ink); - padding: 2px 8px; - margin-bottom: var(--spacing-xs); -} - -.equation-title { - display: block; - font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; - font-size: clamp(1.125rem, 2vw, 1.5rem); - font-weight: 600; - color: var(--color-ink); - margin-bottom: var(--spacing-xs); -} - -.equation-list { - list-style: none; - padding: 0; - margin: 0; - font-size: 0.8125rem; - color: var(--color-charcoal); - line-height: 1.6; - white-space: nowrap; -} - -.equation-operator { +.hero-logos-inline .hero-logos-row { display: flex; align-items: center; - justify-content: center; - font-family: var(--font-display); - font-size: 2.5rem; - font-weight: 300; - color: var(--color-accent); - flex-shrink: 0; - width: 48px; + gap: 8px; } -@media (max-width: 768px) { - .hero-equation { - flex-direction: column; - gap: var(--spacing-sm); - } - - .equation-operator { - font-size: 1.75rem; - width: auto; - padding: var(--spacing-xs) 0; - } -} - -.hero-logos { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} - -.hero-logos-label { - font-size: 0.75rem; - color: var(--color-ash); - letter-spacing: 0.03em; -} - -.hero-logos-row { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} - -.hero-logos-row img { +.hero-logos-inline .hero-logos-row img { border-radius: 4px; + opacity: 0.7; + transition: opacity 0.2s ease; } -.hero-scroll-indicator { - position: absolute; - bottom: var(--spacing-lg); - left: 50%; - transform: translateX(-50%); +.hero-logos-inline .hero-logos-row img:hover { + opacity: 1; +} + +.hero-combined-right { + display: flex; + justify-content: center; +} + +.hero-combined-right .split-comparison { + max-width: 520px; + width: 100%; +} + +.hero-combined-right .split-container { + max-width: 100%; +} + +/* Bias tags at bottom of hero */ +.hero-bias-tags { display: flex; flex-direction: column; align-items: center; gap: var(--spacing-xs); - color: var(--color-ash); - font-size: 0.6875rem; - font-weight: 500; - letter-spacing: 0.1em; - text-transform: uppercase; - opacity: 0; - /* Restore simple CSS animation */ - animation: fadeIn 1s var(--ease-out) 1.5s forwards, float 2s ease-in-out infinite 2s; -} - -/* Removed .revealed dependency for scroll indicator */ - -.hero-scroll-indicator svg { - /* No separate bounce animation needed, handled by float */ + margin-top: var(--spacing-lg); + padding-top: var(--spacing-md); + border-top: 1px solid var(--color-mist); + max-width: var(--width-max); + margin-left: auto; + margin-right: auto; + width: 100%; + padding-bottom: var(--spacing-md); } /* ============================================ diff --git a/public/css/workflow.css b/public/css/workflow.css index 7e17eb64b..ff1c4bd7f 100644 --- a/public/css/workflow.css +++ b/public/css/workflow.css @@ -139,7 +139,7 @@ font-family: var(--font-mono); font-size: 0.75rem; padding: 5px 12px; - background: var(--color-mist); + background: var(--color-cream); border: 1px solid var(--color-mist); border-bottom: none; border-radius: 6px 6px 0 0; @@ -201,14 +201,12 @@ /* Source Window Content */ .source-window { - background: rgba(255, 255, 255, 0.95); + background: var(--color-paper); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--color-mist); border-radius: 8px; - box-shadow: - 0 20px 60px -10px rgba(0,0,0,0.15), - 0 0 0 1px rgba(255,255,255,0.5) inset; + box-shadow: 0 20px 60px -10px rgba(0,0,0,0.15); height: 100%; display: flex; flex-direction: column; @@ -216,7 +214,7 @@ } .source-header { - background: rgba(0,0,0,0.02); + background: var(--color-cream); padding: 12px 16px; display: flex; align-items: center; @@ -259,15 +257,12 @@ } .glass-terminal { - background: rgba(255, 255, 255, 0.9); - /* Glass effect handled by backdrop-filter if supported */ + background: var(--color-paper); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--color-mist); border-radius: 8px; - box-shadow: - 0 20px 60px -10px rgba(0,0,0,0.15), - 0 0 0 1px rgba(255,255,255,0.5) inset; + box-shadow: 0 20px 60px -10px rgba(0,0,0,0.15); height: 100%; display: flex; flex-direction: column; @@ -275,7 +270,7 @@ } .terminal-header { - background: rgba(0,0,0,0.02); + background: var(--color-cream); padding: 12px 16px; display: flex; align-items: center; diff --git a/public/index.html b/public/index.html index a6b9096c2..e86e8df0d 100644 --- a/public/index.html +++ b/public/index.html @@ -38,81 +38,54 @@ - -
- -
-
-

Impeccable

-

Design fluency for Claude Code, Codex, and more.

- Get Started -
-
+ +
+ + + +
- -
-
-

What's Included

+ +
+
+ +
+

Impeccable

+

Design fluency for AI coding tools

- -
-
- Enhanced Skill - frontend-design -
    -
  • Anthropic's original skill
  • -
  • + curated anti-patterns
  • -
  • + 7 deep reference guides
  • -
-
+

Every LLM learned from the same generic templates. Without explicit guidance, they converge on predictable defaults.

- + - -
- Steering Commands - 17 commands -
    -
  • /polish, /audit, /simplify
  • -
  • /bolder, /quieter, /animate
  • -
  • and 11 more...
  • -
-
-
- - - - - -
- -
-
- 01 -

AI Has Design Bias

-
-
-

Every LLM learned from the same generic templates. Without explicit guidance, they converge on the same predictable defaults.

- - -
+ +
+
@@ -160,26 +133,16 @@
- - -
- Common AI defaults: -
- Inter/Roboto fonts - Purple gradients - Cards on cards - Centered everything - Gray text on color - Decorative shadows -
-
-
+
+
- +
+ +
- 02 + 01

The Antidote

@@ -203,10 +166,10 @@
- +
- 03 + 02

The Framework

@@ -226,7 +189,7 @@
- 04 + 03

Commands in Action

See how commands transform your designs

@@ -239,7 +202,7 @@
- 05 + 04

See It In Action

Sample projects transformed with Impeccable commands

@@ -335,7 +298,7 @@
- 06 + 05

Download for Your AI Harness

Same design intelligence, adapted for your workflow. Download and start using in seconds.

@@ -438,7 +401,7 @@
- 07 + 06

Frequently Asked Questions

diff --git a/public/js/components/framework-viz.js b/public/js/components/framework-viz.js index 05e40c9dd..571df58f9 100644 --- a/public/js/components/framework-viz.js +++ b/public/js/components/framework-viz.js @@ -5,13 +5,14 @@ import { commandCategories, commandRelationships } from '../data.js'; +// Colors now reference CSS custom properties for dark mode support const categoryColors = { - diagnostic: { bg: '#fdf4ff', border: '#d946ef', text: '#a21caf' }, - quality: { bg: '#f0fdf4', border: '#22c55e', text: '#15803d' }, - intensity: { bg: '#fffbeb', border: '#f59e0b', text: '#b45309' }, - adaptation: { bg: '#eff6ff', border: '#3b82f6', text: '#1d4ed8' }, - enhancement: { bg: '#fdf2f8', border: '#ec4899', text: '#be185d' }, - system: { bg: '#f9fafb', border: '#6b7280', text: '#374151' } + diagnostic: { bg: 'var(--cat-diagnostic-bg)', border: 'var(--cat-diagnostic-border)', text: 'var(--cat-diagnostic-text)' }, + quality: { bg: 'var(--cat-quality-bg)', border: 'var(--cat-quality-border)', text: 'var(--cat-quality-text)' }, + intensity: { bg: 'var(--cat-intensity-bg)', border: 'var(--cat-intensity-border)', text: 'var(--cat-intensity-text)' }, + adaptation: { bg: 'var(--cat-adaptation-bg)', border: 'var(--cat-adaptation-border)', text: 'var(--cat-adaptation-text)' }, + enhancement: { bg: 'var(--cat-enhancement-bg)', border: 'var(--cat-enhancement-border)', text: 'var(--cat-enhancement-text)' }, + system: { bg: 'var(--cat-system-bg)', border: 'var(--cat-system-border)', text: 'var(--cat-system-text)' } }; const categoryLabels = { diff --git a/public/js/utils/scroll.js b/public/js/utils/scroll.js index c1afd8082..4f31c06bd 100644 --- a/public/js/utils/scroll.js +++ b/public/js/utils/scroll.js @@ -1,69 +1,17 @@ -import Lenis from "lenis"; - -// Check if user prefers reduced motion -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -export function initSmoothScroll() { - // Skip smooth scroll entirely if user prefers reduced motion - if (prefersReducedMotion) { - // Still handle anchor links but with instant scroll - document.querySelectorAll('a[href^="#"]').forEach((anchor) => { - anchor.addEventListener("click", (e) => { - e.preventDefault(); - const target = document.querySelector(anchor.getAttribute("href")); - if (target) { - target.scrollIntoView({ behavior: 'auto', block: 'start' }); - } - }); - }); - return null; - } - - const lenis = new Lenis({ - duration: 1.2, - easing: (t) => Math.min(1, 1.001 - 2 ** (-10 * t)), - orientation: "vertical", - gestureOrientation: "vertical", - smoothWheel: true, - wheelMultiplier: 1, - touchMultiplier: 2, - }); - - function raf(time) { - lenis.raf(time); - requestAnimationFrame(raf); - } - - requestAnimationFrame(raf); - - // Handle anchor links +// Instant anchor scroll - no smooth scrolling for better UX on long pages +export function initAnchorScroll() { document.querySelectorAll('a[href^="#"]').forEach((anchor) => { anchor.addEventListener("click", (e) => { e.preventDefault(); const target = document.querySelector(anchor.getAttribute("href")); if (target) { - lenis.scrollTo(target, { offset: -40 }); + // Instant jump with small offset for visual breathing room + const offset = 40; + const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset; + window.scrollTo({ top: targetPosition, behavior: 'auto' }); } }); }); - - return lenis; -} - -export function initScrollIndicator() { - const indicator = document.querySelector(".hero-scroll-indicator"); - if (!indicator) return; - - function update() { - if (window.scrollY > 20) { - indicator.classList.add("hidden"); - } else { - indicator.classList.remove("hidden"); - } - } - - window.addEventListener("scroll", update, { passive: true }); - update(); } export function initHashTracking() { @@ -109,12 +57,14 @@ export function initHashTracking() { } }, { passive: true }); - // Handle initial hash on page load + // Handle initial hash on page load - instant jump if (window.location.hash) { const target = document.querySelector(window.location.hash); if (target) { setTimeout(() => { - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + const offset = 40; + const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset; + window.scrollTo({ top: targetPosition, behavior: 'auto' }); }, 100); } } diff --git a/public/js/utils/theme.js b/public/js/utils/theme.js new file mode 100644 index 000000000..6209dd1a8 --- /dev/null +++ b/public/js/utils/theme.js @@ -0,0 +1,56 @@ +// Theme toggle - supports light, dark, and system preference + +const STORAGE_KEY = 'impeccable-theme'; + +function getStoredTheme() { + return localStorage.getItem(STORAGE_KEY); +} + +function setStoredTheme(theme) { + localStorage.setItem(STORAGE_KEY, theme); +} + +function applyTheme(theme) { + const html = document.documentElement; + + // Remove both classes first + html.classList.remove('light', 'dark'); + + if (theme === 'light') { + html.classList.add('light'); + } else if (theme === 'dark') { + html.classList.add('dark'); + } + // 'system' = no class, falls back to media query + + // Update active state on buttons + document.querySelectorAll('.theme-toggle-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.theme === theme); + }); +} + +export function initThemeToggle() { + const toggle = document.querySelector('.theme-toggle'); + if (!toggle) return; + + // Get stored theme or default to system + const storedTheme = getStoredTheme() || 'system'; + applyTheme(storedTheme); + + // Handle button clicks + toggle.addEventListener('click', (e) => { + const btn = e.target.closest('.theme-toggle-btn'); + if (!btn) return; + + const theme = btn.dataset.theme; + setStoredTheme(theme); + applyTheme(theme); + }); + + // Listen for system preference changes (only matters when in 'system' mode) + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + if (getStoredTheme() === 'system' || !getStoredTheme()) { + applyTheme('system'); + } + }); +}