From f7ad2dcf87c6971a8f3ded2b058bd38f6d634013 Mon Sep 17 00:00:00 2001 From: Clovis Muneza Date: Fri, 6 Mar 2026 12:35:49 -0500 Subject: [PATCH 1/2] Add landing page for GitHub Pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Static single-page marketing site showcasing pv's capabilities. Dark theme with blueprint grid pattern, animated terminal demo, feature cards, installation instructions, and copy-to-clipboard. No build tools or dependencies — pure HTML, CSS, and JS. --- docs/index.html | 270 +++++++++++++++ docs/script.js | 214 ++++++++++++ docs/style.css | 861 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1345 insertions(+) create mode 100644 docs/index.html create mode 100644 docs/script.js create mode 100644 docs/style.css diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..7c1e810 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,270 @@ + + + + + + pv - PHP dev servers, instantly + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ +
+ + Powered by FrankenPHP +
+ + +

+ PHP dev servers,
+ instantly. +

+ + +

+ No dnsmasq. No Docker. No config files. Link a project and it's live at + https://project.test — HTTPS included. +

+ + + + + +
+
+ + + + zsh +
+
+ + + +
+
+
+
+ + +
+
+ +

+ Running PHP projects locally shouldn't require juggling dnsmasq, Docker, and Traefik. +

+

+ pv replaces all of that with a single binary. Install it, link your project directory, and it's instantly available at a .test domain with HTTPS. No containers, no proxy chains, no config files to maintain. +

+
+
+ + +
+
+ +

Everything you need. Nothing you don't.

+

A complete local PHP environment in one tool.

+
+
+ +
+ + + +

One command setup

+

+ pv install gets you FrankenPHP, PHP, Composer, and Mago — ready to serve PHP projects immediately. +

+
+ +
+ + + +

HTTPS out of the box

+

+ Every project gets an automatic .test domain with a trusted local certificate. Zero config. +

+
+ +
+ + + +

Multi-version PHP

+

+ Install PHP 8.2, 8.3, 8.4 side by side. Switch globally or per-project. No phpenv, no phpbrew. +

+
+ +
+ + + +

Auto project detection

+

+ pv link detects Laravel, Laravel Octane, and generic PHP automatically and generates the right server config. +

+
+
+
+ + +
+
+ +

Up and running in three commands.

+

From zero to a live local site in under a minute.

+
+ +
+ +
+
1
+
+

Install

+

Downloads pv and sets up FrankenPHP, PHP, Composer, and Mago.

+
+ $ curl -fsSL https://raw.githubusercontent.com/prvious/pv/main/install.sh | bash +
+
+
+ +
+
2
+
+

Link your project

+

Auto-detects your project type and sets up the server config.

+
+ $ pv link ~/code/my-laravel-app +
+
+
+ +
+
3
+
+

Start

+

Your app is now live at https://my-laravel-app.test with HTTPS.

+
+ $ pv start +
+
+
+
+
+ + +
+
+ +

Install pv

+

One command. No dependencies.

+
+ +
+
+ $ curl -fsSL https://raw.githubusercontent.com/prvious/pv/main/install.sh | bash +
+ +
+ +

+ Requires macOS. Source available on + GitHub. +

+
+ + + + + + + + diff --git a/docs/script.js b/docs/script.js new file mode 100644 index 0000000..317dca2 --- /dev/null +++ b/docs/script.js @@ -0,0 +1,214 @@ +/* ========================================================================== + pv — Landing Page Scripts + Terminal typewriter animation + copy-to-clipboard + ========================================================================== */ + +(function () { + 'use strict'; + + // -------------------------------------------------------------------------- + // Terminal Typewriter Animation + // -------------------------------------------------------------------------- + + const sequences = [ + { + command: 'pv install', + output: [ + '\u2713 FrankenPHP installed', + '\u2713 PHP 8.4 ready', + '\u2713 Composer installed', + ], + pauseAfter: 1800, + }, + { + command: 'pv link ~/code/my-app', + output: [ + '\u2713 Linked my-app', + '\u2713 Starting server...', + '\u2713 Live at https://my-app.test', + ], + pauseAfter: 2200, + }, + { + command: 'pv php install 8.3', + output: [ + '\u2713 Downloading PHP 8.3...', + '\u2713 PHP 8.3 installed', + ], + pauseAfter: 1800, + }, + ]; + + const TYPING_SPEED = 45; // ms per character + const OUTPUT_LINE_DELAY = 200; // ms between output lines + const PAUSE_BEFORE_CLEAR = 600; + + function sleep(ms) { + return new Promise(function (resolve) { setTimeout(resolve, ms); }); + } + + function initTerminal() { + var body = document.getElementById('terminal-body'); + if (!body) return; + + var seqIndex = 0; + + async function runSequence() { + while (true) { + var seq = sequences[seqIndex]; + body.innerHTML = ''; + + // Create prompt line with cursor + var promptLine = document.createElement('span'); + promptLine.className = 'terminal__line terminal__line--prompt'; + body.appendChild(promptLine); + + var cursor = document.createElement('span'); + cursor.className = 'terminal__cursor'; + body.appendChild(cursor); + + // Type out the command character by character + for (var i = 0; i < seq.command.length; i++) { + promptLine.textContent += seq.command[i]; + await sleep(TYPING_SPEED); + } + + await sleep(300); + + // Remove cursor temporarily + cursor.remove(); + + // Print output lines one by one + for (var j = 0; j < seq.output.length; j++) { + var outputLine = document.createElement('span'); + outputLine.className = 'terminal__line terminal__line--output'; + outputLine.textContent = seq.output[j]; + body.appendChild(outputLine); + await sleep(OUTPUT_LINE_DELAY); + } + + // Add blinking cursor on new prompt line + var newPrompt = document.createElement('span'); + newPrompt.className = 'terminal__line terminal__line--prompt'; + body.appendChild(newPrompt); + + var newCursor = document.createElement('span'); + newCursor.className = 'terminal__cursor'; + body.appendChild(newCursor); + + // Pause to let user read + await sleep(seq.pauseAfter); + + // Move to next sequence + seqIndex = (seqIndex + 1) % sequences.length; + await sleep(PAUSE_BEFORE_CLEAR); + } + } + + runSequence(); + } + + // -------------------------------------------------------------------------- + // Copy to Clipboard + // -------------------------------------------------------------------------- + + function initCopyButton() { + var btn = document.getElementById('copy-btn'); + if (!btn) return; + + var command = 'curl -fsSL https://raw.githubusercontent.com/prvious/pv/main/install.sh | bash'; + + var copyIcon = ''; + var checkIcon = ''; + + btn.addEventListener('click', function () { + // Clipboard API requires secure context (HTTPS or localhost) + // Falls back gracefully if unavailable + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(command).then(function () { + btn.innerHTML = checkIcon; + btn.classList.add('install__copy--copied'); + setTimeout(function () { + btn.innerHTML = copyIcon; + btn.classList.remove('install__copy--copied'); + }, 2000); + }); + } else { + // Fallback: select text from a temporary textarea + var ta = document.createElement('textarea'); + ta.value = command; + ta.style.position = 'fixed'; + ta.style.left = '-9999px'; + document.body.appendChild(ta); + ta.select(); + try { + document.execCommand('copy'); + btn.innerHTML = checkIcon; + btn.classList.add('install__copy--copied'); + setTimeout(function () { + btn.innerHTML = copyIcon; + btn.classList.remove('install__copy--copied'); + }, 2000); + } catch (e) { + // silently fail + } + document.body.removeChild(ta); + } + }); + } + + // -------------------------------------------------------------------------- + // Scroll Fade-In Animation + // -------------------------------------------------------------------------- + + function initScrollAnimations() { + var elements = document.querySelectorAll('.fade-in'); + if (!elements.length) return; + + var observer = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + observer.unobserve(entry.target); + } + }); + }, { + threshold: 0.1, + rootMargin: '0px 0px -40px 0px', + }); + + elements.forEach(function (el) { + observer.observe(el); + }); + } + + // -------------------------------------------------------------------------- + // Smooth Scroll for CTA + // -------------------------------------------------------------------------- + + function initSmoothScroll() { + var links = document.querySelectorAll('a[href^="#"]'); + links.forEach(function (link) { + link.addEventListener('click', function (e) { + var targetId = link.getAttribute('href'); + if (targetId === '#') return; + var target = document.querySelector(targetId); + if (target) { + e.preventDefault(); + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); + }); + } + + // -------------------------------------------------------------------------- + // Init + // -------------------------------------------------------------------------- + + document.addEventListener('DOMContentLoaded', function () { + initTerminal(); + initCopyButton(); + initScrollAnimations(); + initSmoothScroll(); + }); +})(); diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..0013b1c --- /dev/null +++ b/docs/style.css @@ -0,0 +1,861 @@ +/* ========================================================================== + pv — Landing Page Styles + Pure CSS, no frameworks, no build tools. + ========================================================================== */ + +/* -------------------------------------------------------------------------- + 1. Custom Properties + -------------------------------------------------------------------------- */ +:root { + /* Colors */ + --bg-hero: #0b0f1a; + --bg-body: #0d1117; + --bg-alt: #111827; + --bg-terminal: #161b22; + --accent: #155dfc; + --accent-hover: #3b82f6; + --accent-glow: rgba(21, 93, 252, 0.25); + --text-primary: #f0f6fc; + --text-secondary: #8b949e; + --text-muted: #6e7681; + --border: rgba(255, 255, 255, 0.08); + --border-hover: rgba(21, 93, 252, 0.4); + + /* Terminal dots */ + --dot-red: #ff5f56; + --dot-yellow: #ffbd2e; + --dot-green: #27c93f; + + /* Typography */ + --font-body: 'Inter', system-ui, -apple-system, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace; + + /* Spacing */ + --section-padding: 96px 24px; + --section-padding-mobile: 64px 16px; + --container-max: 1080px; + --container-narrow: 720px; +} + +/* -------------------------------------------------------------------------- + 2. Reset & Base + -------------------------------------------------------------------------- */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-body); + font-size: 16px; + line-height: 1.6; + color: var(--text-primary); + background-color: var(--bg-body); +} + +a { + color: var(--accent); + text-decoration: none; + transition: color 0.2s ease; +} + +a:hover { + color: var(--accent-hover); +} + +img { + max-width: 100%; + display: block; +} + +/* -------------------------------------------------------------------------- + 3. Typography + -------------------------------------------------------------------------- */ +h1, h2, h3 { + font-weight: 700; + line-height: 1.2; + letter-spacing: -0.02em; +} + +h1 { + font-size: 3.5rem; +} + +h2 { + font-size: 2.25rem; + margin-bottom: 16px; +} + +h3 { + font-size: 1.25rem; + margin-bottom: 8px; +} + +p { + color: var(--text-secondary); +} + +code { + font-family: var(--font-mono); + font-size: 0.9em; + background: rgba(255, 255, 255, 0.06); + padding: 2px 6px; + border-radius: 4px; +} + +/* -------------------------------------------------------------------------- + 4. Layout Utilities + -------------------------------------------------------------------------- */ +.container { + max-width: var(--container-max); + margin: 0 auto; + padding: 0 24px; +} + +.container--narrow { + max-width: var(--container-narrow); +} + +.section { + padding: var(--section-padding); +} + +.section--alt { + background-color: var(--bg-alt); +} + +.text-center { + text-align: center; +} + +.section-label { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 0.8125rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--accent); + background: rgba(21, 93, 252, 0.1); + border: 1px solid rgba(21, 93, 252, 0.25); + padding: 6px 14px; + border-radius: 100px; + margin-bottom: 24px; +} + +.section-heading { + font-size: 2.25rem; + margin-bottom: 16px; + color: var(--text-primary); +} + +.section-subheading { + font-size: 1.125rem; + color: var(--text-secondary); + max-width: 560px; + margin: 0 auto 48px; + line-height: 1.7; +} + +/* -------------------------------------------------------------------------- + 5. Navbar + -------------------------------------------------------------------------- */ +.navbar { + position: sticky; + top: 0; + z-index: 100; + background: rgba(11, 15, 26, 0.85); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); + padding: 0 24px; +} + +.navbar__inner { + max-width: var(--container-max); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + height: 64px; +} + +.navbar__logo { + font-family: var(--font-mono); + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + text-decoration: none; + display: flex; + align-items: center; + gap: 2px; +} + +.navbar__logo-accent { + color: var(--accent); +} + +.navbar__github { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--text-secondary); + font-size: 0.875rem; + font-weight: 500; + padding: 8px 16px; + border: 1px solid var(--border); + border-radius: 8px; + transition: border-color 0.2s ease, color 0.2s ease; +} + +.navbar__github:hover { + border-color: var(--border-hover); + color: var(--text-primary); +} + +.navbar__github svg { + width: 20px; + height: 20px; + fill: currentColor; +} + +/* -------------------------------------------------------------------------- + 6. Hero + -------------------------------------------------------------------------- */ +.hero { + position: relative; + background-color: var(--bg-hero); + overflow: hidden; + padding: 80px 24px 0; +} + +.hero__bg { + position: absolute; + inset: 0; + background-image: + radial-gradient(ellipse 80% 50% at 50% 0%, rgba(21, 93, 252, 0.18) 0%, transparent 70%), + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Cdefs%3E%3Cpattern id='g' width='60' height='60' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 60 0 L 0 0 0 60' fill='none' stroke='rgba(255,255,255,0.04)' stroke-width='0.5'/%3E%3Cline x1='-4' y1='0' x2='4' y2='0' stroke='rgba(255,255,255,0.12)' stroke-width='0.8'/%3E%3Cline x1='0' y1='-4' x2='0' y2='4' stroke='rgba(255,255,255,0.12)' stroke-width='0.8'/%3E%3Crect x='-1' y='-1' width='2' height='2' fill='rgba(255,255,255,0.08)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='60' height='60' fill='url(%23g)'/%3E%3C/svg%3E"); + background-size: auto, 60px 60px; + pointer-events: none; +} + +.hero__fade { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 200px; + background: linear-gradient(to bottom, transparent 0%, var(--bg-body) 100%); + pointer-events: none; +} + +.hero__content { + position: relative; + z-index: 2; + max-width: 800px; + margin: 0 auto; + text-align: center; +} + +.hero__badge { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.8125rem; + font-weight: 600; + color: var(--accent); + background: rgba(21, 93, 252, 0.1); + border: 1px solid rgba(21, 93, 252, 0.3); + padding: 6px 16px; + border-radius: 100px; + margin-bottom: 32px; +} + +.hero__title { + font-size: 3.5rem; + font-weight: 800; + line-height: 1.1; + letter-spacing: -0.03em; + margin-bottom: 20px; + color: var(--text-primary); +} + +.hero__title-accent { + color: var(--accent); +} + +.hero__subtitle { + font-size: 1.2rem; + color: var(--text-secondary); + line-height: 1.7; + max-width: 600px; + margin: 0 auto 36px; +} + +.hero__subtitle code { + color: var(--accent); + background: rgba(21, 93, 252, 0.12); + border: 1px solid rgba(21, 93, 252, 0.2); +} + +.hero__ctas { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + margin-bottom: 56px; + flex-wrap: wrap; +} + +/* -------------------------------------------------------------------------- + 7. Buttons + -------------------------------------------------------------------------- */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + font-family: var(--font-body); + font-size: 1rem; + font-weight: 600; + padding: 12px 28px; + border-radius: 10px; + border: none; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; +} + +.btn--primary { + background: var(--accent); + color: #fff; + box-shadow: 0 0 24px var(--accent-glow); +} + +.btn--primary:hover { + background: var(--accent-hover); + color: #fff; + box-shadow: 0 0 40px var(--accent-glow); + transform: translateY(-1px); +} + +.btn--secondary { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border); +} + +.btn--secondary:hover { + border-color: var(--border-hover); + color: var(--text-primary); +} + +.btn--secondary svg { + width: 18px; + height: 18px; + fill: currentColor; +} + +/* -------------------------------------------------------------------------- + 8. Terminal Window + -------------------------------------------------------------------------- */ +.terminal { + max-width: 620px; + margin: 0 auto; + border-radius: 12px; + overflow: hidden; + background: var(--bg-terminal); + border: 1px solid var(--border); + box-shadow: + 0 4px 6px rgba(0, 0, 0, 0.3), + 0 24px 80px rgba(0, 0, 0, 0.4); + text-align: left; + position: relative; + z-index: 2; + margin-bottom: -60px; +} + +.terminal__header { + display: flex; + align-items: center; + gap: 8px; + padding: 14px 16px; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid var(--border); +} + +.terminal__dot { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.terminal__dot--red { background: var(--dot-red); } +.terminal__dot--yellow { background: var(--dot-yellow); } +.terminal__dot--green { background: var(--dot-green); } + +.terminal__title { + flex: 1; + text-align: center; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--text-muted); + margin-right: 44px; /* offset for dots */ +} + +.terminal__body { + padding: 20px; + min-height: 180px; + font-family: var(--font-mono); + font-size: 0.875rem; + line-height: 1.7; +} + +.terminal__line { + display: block; + white-space: pre; + color: var(--text-secondary); +} + +.terminal__line--prompt::before { + content: '$ '; + color: var(--accent); + font-weight: 700; +} + +.terminal__line--output { + color: var(--dot-green); +} + +.terminal__line--output::before { + content: ' '; +} + +.terminal__cursor { + display: inline-block; + width: 8px; + height: 18px; + background: var(--accent); + vertical-align: text-bottom; + animation: blink 1s step-end infinite; + margin-left: 1px; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* -------------------------------------------------------------------------- + 9. Problem Section + -------------------------------------------------------------------------- */ +.problem { + padding: var(--section-padding); + padding-top: 120px; +} + +.problem__inner { + max-width: var(--container-narrow); + margin: 0 auto; + text-align: center; +} + +.problem__quote { + font-size: 1.75rem; + font-weight: 700; + color: var(--text-primary); + line-height: 1.4; + margin-bottom: 24px; + letter-spacing: -0.01em; +} + +.problem__answer { + font-size: 1.125rem; + color: var(--text-secondary); + line-height: 1.7; +} + +.problem__answer strong { + color: var(--accent); + font-weight: 600; +} + +/* -------------------------------------------------------------------------- + 10. Features Section + -------------------------------------------------------------------------- */ +.features { + padding: var(--section-padding); + background: var(--bg-alt); +} + +.features__grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + max-width: var(--container-max); + margin: 0 auto; +} + +.feature-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + border-radius: 12px; + padding: 32px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.feature-card:hover { + border-color: var(--border-hover); + box-shadow: 0 0 40px rgba(21, 93, 252, 0.08); +} + +.feature-card__icon { + font-size: 1.75rem; + margin-bottom: 16px; + display: block; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(21, 93, 252, 0.1); + border-radius: 10px; +} + +.feature-card__title { + font-size: 1.125rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; +} + +.feature-card__desc { + font-size: 0.9375rem; + color: var(--text-secondary); + line-height: 1.6; +} + +/* -------------------------------------------------------------------------- + 11. How It Works + -------------------------------------------------------------------------- */ +.steps { + padding: var(--section-padding); +} + +.steps__list { + max-width: 640px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 48px; +} + +.step { + display: flex; + gap: 24px; +} + +.step__number { + flex-shrink: 0; + width: 48px; + height: 48px; + background: rgba(21, 93, 252, 0.12); + border: 1px solid rgba(21, 93, 252, 0.3); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-mono); + font-size: 1.125rem; + font-weight: 700; + color: var(--accent); +} + +.step__content { + flex: 1; + min-width: 0; +} + +.step__title { + font-size: 1.125rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; +} + +.step__desc { + font-size: 0.9375rem; + color: var(--text-secondary); + margin-bottom: 16px; + line-height: 1.6; +} + +.step__code { + background: var(--bg-terminal); + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px 20px; + font-family: var(--font-mono); + font-size: 0.875rem; + color: var(--text-primary); + overflow-x: auto; +} + +.step__code .prompt { + color: var(--accent); + font-weight: 700; + user-select: none; +} + +/* -------------------------------------------------------------------------- + 12. Install Section + -------------------------------------------------------------------------- */ +.install { + padding: var(--section-padding); + background: var(--bg-alt); +} + +.install__box { + max-width: 640px; + margin: 0 auto; + background: var(--bg-terminal); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px 24px; + display: flex; + align-items: center; + gap: 16px; +} + +.install__command { + flex: 1; + font-family: var(--font-mono); + font-size: 0.875rem; + color: var(--text-primary); + overflow-x: auto; + white-space: nowrap; +} + +.install__command .prompt { + color: var(--accent); + font-weight: 700; + user-select: none; +} + +.install__copy { + flex-shrink: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--border); + border-radius: 8px; + cursor: pointer; + color: var(--text-secondary); + font-size: 1rem; + transition: all 0.2s ease; +} + +.install__copy:hover { + background: rgba(255, 255, 255, 0.1); + border-color: var(--border-hover); + color: var(--text-primary); +} + +.install__copy svg { + width: 18px; + height: 18px; + stroke: currentColor; + fill: none; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} + +.install__copy--copied { + color: var(--dot-green); + border-color: var(--dot-green); +} + +.install__copy--copied svg { + stroke: var(--dot-green); +} + +.install__note { + text-align: center; + margin-top: 20px; + font-size: 0.8125rem; + color: var(--text-muted); +} + +.install__note a { + color: var(--text-secondary); + text-decoration: underline; + text-underline-offset: 2px; +} + +.install__note a:hover { + color: var(--text-primary); +} + +/* -------------------------------------------------------------------------- + 13. Footer + -------------------------------------------------------------------------- */ +.footer { + background: var(--bg-hero); + border-top: 1px solid var(--border); + padding: 40px 24px; +} + +.footer__inner { + max-width: var(--container-max); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 16px; +} + +.footer__left { + display: flex; + align-items: center; + gap: 16px; +} + +.footer__logo { + font-family: var(--font-mono); + font-size: 1.25rem; + font-weight: 700; + color: var(--text-primary); +} + +.footer__logo-accent { + color: var(--accent); +} + +.footer__tagline { + font-size: 0.875rem; + color: var(--text-muted); +} + +.footer__right { + display: flex; + align-items: center; + gap: 24px; + font-size: 0.875rem; + color: var(--text-muted); +} + +.footer__right a { + color: var(--text-secondary); + transition: color 0.2s ease; +} + +.footer__right a:hover { + color: var(--text-primary); +} + +/* -------------------------------------------------------------------------- + 14. Responsive + -------------------------------------------------------------------------- */ +@media (max-width: 768px) { + :root { + --section-padding: 64px 16px; + } + + .hero { + padding: 48px 16px 0; + } + + .hero__title { + font-size: 2.25rem; + } + + .hero__subtitle { + font-size: 1.05rem; + } + + .hero__ctas { + flex-direction: column; + gap: 12px; + } + + .btn { + width: 100%; + justify-content: center; + } + + .terminal { + max-width: 100%; + } + + .terminal__body { + font-size: 0.8rem; + padding: 16px; + min-height: 160px; + } + + .problem__quote { + font-size: 1.375rem; + } + + .features__grid { + grid-template-columns: 1fr; + gap: 16px; + } + + .section-heading { + font-size: 1.75rem; + } + + .step { + flex-direction: column; + gap: 16px; + } + + .step__code { + font-size: 0.8rem; + } + + .install__box { + padding: 16px; + } + + .install__command { + font-size: 0.75rem; + } + + .footer__inner { + flex-direction: column; + text-align: center; + } + + .footer__left { + flex-direction: column; + gap: 8px; + } +} + +/* -------------------------------------------------------------------------- + 15. Animations + -------------------------------------------------------------------------- */ +.fade-in { + opacity: 0; + transform: translateY(20px); + transition: opacity 0.6s ease, transform 0.6s ease; +} + +.fade-in.visible { + opacity: 1; + transform: translateY(0); +} From e68052c17c9c046bc88f367b51a86c36db32c677 Mon Sep 17 00:00:00 2001 From: Clovis Muneza Date: Fri, 6 Mar 2026 12:40:29 -0500 Subject: [PATCH 2/2] Add color palette switcher for exploring accent colors Floating button in the bottom-right cycles through 10 palettes (blue, emerald, violet, rose, amber, cyan, orange, pink, teal, indigo). Refactors all hardcoded accent rgba values to CSS custom properties so the JS switcher can update everything at once. --- docs/index.html | 8 +-- docs/script.js | 136 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/style.css | 115 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 244 insertions(+), 15 deletions(-) diff --git a/docs/index.html b/docs/index.html index 7c1e810..5a2c9cf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -134,7 +134,7 @@

Everything you need. Nothing you don't.

- +

One command setup

@@ -144,7 +144,7 @@

One command setup

- +

HTTPS out of the box

@@ -154,7 +154,7 @@

HTTPS out of the box

- +

Multi-version PHP

@@ -164,7 +164,7 @@

Multi-version PHP

- +

Auto project detection

diff --git a/docs/script.js b/docs/script.js index 317dca2..f80aa38 100644 --- a/docs/script.js +++ b/docs/script.js @@ -201,6 +201,141 @@ }); } + // -------------------------------------------------------------------------- + // Color Palette Switcher + // -------------------------------------------------------------------------- + + var palettes = [ + { + name: 'Electric Blue', + accent: '#155dfc', + accentHover: '#3b82f6', + swatch: '#155dfc', + }, + { + name: 'Emerald', + accent: '#10b981', + accentHover: '#34d399', + swatch: '#10b981', + }, + { + name: 'Violet', + accent: '#8b5cf6', + accentHover: '#a78bfa', + swatch: '#8b5cf6', + }, + { + name: 'Rose', + accent: '#f43f5e', + accentHover: '#fb7185', + swatch: '#f43f5e', + }, + { + name: 'Amber', + accent: '#f59e0b', + accentHover: '#fbbf24', + swatch: '#f59e0b', + }, + { + name: 'Cyan', + accent: '#06b6d4', + accentHover: '#22d3ee', + swatch: '#06b6d4', + }, + { + name: 'Orange', + accent: '#f97316', + accentHover: '#fb923c', + swatch: '#f97316', + }, + { + name: 'Pink', + accent: '#ec4899', + accentHover: '#f472b6', + swatch: '#ec4899', + }, + { + name: 'Teal', + accent: '#14b8a6', + accentHover: '#2dd4bf', + swatch: '#14b8a6', + }, + { + name: 'Indigo', + accent: '#6366f1', + accentHover: '#818cf8', + swatch: '#6366f1', + }, + ]; + + // Convert hex to RGB components + function hexToRgb(hex) { + var r = parseInt(hex.slice(1, 3), 16); + var g = parseInt(hex.slice(3, 5), 16); + var b = parseInt(hex.slice(5, 7), 16); + return { r: r, g: g, b: b }; + } + + function applyPalette(index) { + var p = palettes[index]; + var root = document.documentElement; + var rgb = hexToRgb(p.accent); + var r = rgb.r, g = rgb.g, b = rgb.b; + + root.style.setProperty('--accent', p.accent); + root.style.setProperty('--accent-hover', p.accentHover); + root.style.setProperty('--accent-glow', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.25)'); + root.style.setProperty('--accent-bg-subtle', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.1)'); + root.style.setProperty('--accent-bg-medium', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.12)'); + root.style.setProperty('--accent-border', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.25)'); + root.style.setProperty('--accent-border-strong', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.3)'); + root.style.setProperty('--accent-radial', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.18)'); + root.style.setProperty('--accent-card-glow', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.08)'); + root.style.setProperty('--accent-code-bg', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.12)'); + root.style.setProperty('--accent-code-border', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.2)'); + root.style.setProperty('--border-hover', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.4)'); + } + + function initColorSwitcher() { + var currentIndex = 0; + + // Build the widget + var widget = document.createElement('div'); + widget.className = 'color-switcher'; + + // Swatch (the clickable circle) + var swatch = document.createElement('button'); + swatch.className = 'color-switcher__swatch'; + swatch.style.background = palettes[0].swatch; + swatch.setAttribute('aria-label', 'Switch color palette'); + swatch.title = palettes[0].name; + + // Label + var label = document.createElement('span'); + label.className = 'color-switcher__label'; + label.textContent = palettes[0].name; + + widget.appendChild(label); + widget.appendChild(swatch); + document.body.appendChild(widget); + + swatch.addEventListener('click', function () { + currentIndex = (currentIndex + 1) % palettes.length; + applyPalette(currentIndex); + + // Update swatch color and label + swatch.style.background = palettes[currentIndex].swatch; + swatch.title = palettes[currentIndex].name; + label.textContent = palettes[currentIndex].name; + + // Flash the label visible + label.classList.remove('color-switcher__label--visible'); + // Force reflow + void label.offsetWidth; + label.classList.add('color-switcher__label--visible'); + }); + } + // -------------------------------------------------------------------------- // Init // -------------------------------------------------------------------------- @@ -210,5 +345,6 @@ initCopyButton(); initScrollAnimations(); initSmoothScroll(); + initColorSwitcher(); }); })(); diff --git a/docs/style.css b/docs/style.css index 0013b1c..6c211df 100644 --- a/docs/style.css +++ b/docs/style.css @@ -15,6 +15,14 @@ --accent: #155dfc; --accent-hover: #3b82f6; --accent-glow: rgba(21, 93, 252, 0.25); + --accent-bg-subtle: rgba(21, 93, 252, 0.1); + --accent-bg-medium: rgba(21, 93, 252, 0.12); + --accent-border: rgba(21, 93, 252, 0.25); + --accent-border-strong: rgba(21, 93, 252, 0.3); + --accent-radial: rgba(21, 93, 252, 0.18); + --accent-card-glow: rgba(21, 93, 252, 0.08); + --accent-code-bg: rgba(21, 93, 252, 0.12); + --accent-code-border: rgba(21, 93, 252, 0.2); --text-primary: #f0f6fc; --text-secondary: #8b949e; --text-muted: #6e7681; @@ -144,8 +152,8 @@ code { text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); - background: rgba(21, 93, 252, 0.1); - border: 1px solid rgba(21, 93, 252, 0.25); + background: var(--accent-bg-subtle); + border: 1px solid var(--accent-border); padding: 6px 14px; border-radius: 100px; margin-bottom: 24px; @@ -241,7 +249,7 @@ code { position: absolute; inset: 0; background-image: - radial-gradient(ellipse 80% 50% at 50% 0%, rgba(21, 93, 252, 0.18) 0%, transparent 70%), + radial-gradient(ellipse 80% 50% at 50% 0%, var(--accent-radial) 0%, transparent 70%), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Cdefs%3E%3Cpattern id='g' width='60' height='60' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 60 0 L 0 0 0 60' fill='none' stroke='rgba(255,255,255,0.04)' stroke-width='0.5'/%3E%3Cline x1='-4' y1='0' x2='4' y2='0' stroke='rgba(255,255,255,0.12)' stroke-width='0.8'/%3E%3Cline x1='0' y1='-4' x2='0' y2='4' stroke='rgba(255,255,255,0.12)' stroke-width='0.8'/%3E%3Crect x='-1' y='-1' width='2' height='2' fill='rgba(255,255,255,0.08)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='60' height='60' fill='url(%23g)'/%3E%3C/svg%3E"); background-size: auto, 60px 60px; pointer-events: none; @@ -272,8 +280,8 @@ code { font-size: 0.8125rem; font-weight: 600; color: var(--accent); - background: rgba(21, 93, 252, 0.1); - border: 1px solid rgba(21, 93, 252, 0.3); + background: var(--accent-bg-subtle); + border: 1px solid var(--accent-border-strong); padding: 6px 16px; border-radius: 100px; margin-bottom: 32px; @@ -302,8 +310,8 @@ code { .hero__subtitle code { color: var(--accent); - background: rgba(21, 93, 252, 0.12); - border: 1px solid rgba(21, 93, 252, 0.2); + background: var(--accent-code-bg); + border: 1px solid var(--accent-code-border); } .hero__ctas { @@ -513,7 +521,7 @@ code { .feature-card:hover { border-color: var(--border-hover); - box-shadow: 0 0 40px rgba(21, 93, 252, 0.08); + box-shadow: 0 0 40px var(--accent-card-glow); } .feature-card__icon { @@ -525,8 +533,9 @@ code { display: flex; align-items: center; justify-content: center; - background: rgba(21, 93, 252, 0.1); + background: var(--accent-bg-subtle); border-radius: 10px; + color: var(--accent); } .feature-card__title { @@ -566,8 +575,8 @@ code { flex-shrink: 0; width: 48px; height: 48px; - background: rgba(21, 93, 252, 0.12); - border: 1px solid rgba(21, 93, 252, 0.3); + background: var(--accent-bg-medium); + border: 1px solid var(--accent-border-strong); border-radius: 12px; display: flex; align-items: center; @@ -859,3 +868,87 @@ code { opacity: 1; transform: translateY(0); } + +/* -------------------------------------------------------------------------- + 16. Color Switcher Widget + -------------------------------------------------------------------------- */ +.color-switcher { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 9999; + display: flex; + align-items: center; + gap: 12px; +} + +.color-switcher__swatch { + width: 48px; + height: 48px; + border-radius: 50%; + border: 3px solid rgba(255, 255, 255, 0.2); + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(0, 0, 0, 0.2); + outline: none; + -webkit-appearance: none; + appearance: none; +} + +.color-switcher__swatch:hover { + transform: scale(1.12); + border-color: rgba(255, 255, 255, 0.5); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.5), + 0 0 0 1px rgba(0, 0, 0, 0.3); +} + +.color-switcher__swatch:active { + transform: scale(0.95); +} + +.color-switcher__label { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 600; + color: var(--text-primary); + background: var(--bg-terminal); + border: 1px solid var(--border); + padding: 6px 12px; + border-radius: 8px; + white-space: nowrap; + opacity: 0; + transform: translateX(8px); + transition: opacity 0.3s ease, transform 0.3s ease; + pointer-events: none; +} + +.color-switcher__label--visible { + opacity: 1; + transform: translateX(0); + animation: label-fade 2s ease forwards; +} + +@keyframes label-fade { + 0% { opacity: 1; transform: translateX(0); } + 70% { opacity: 1; transform: translateX(0); } + 100% { opacity: 0; transform: translateX(8px); } +} + +@media (max-width: 768px) { + .color-switcher { + bottom: 16px; + right: 16px; + } + + .color-switcher__swatch { + width: 40px; + height: 40px; + } + + .color-switcher__label { + display: none; + } +}