From 87faa3367ede46690e5b8a3c02a109a258cbf04b Mon Sep 17 00:00:00 2001 From: autocommit Date: Mon, 18 May 2026 00:27:14 -0700 Subject: [PATCH] =?UTF-8?q?feat(vip):=20=E2=9C=A8=20Add=20VIP=20quote=20pa?= =?UTF-8?q?ge=20component=20and=20route=20to=20/quotes/vip=20in=20index.ht?= =?UTF-8?q?ml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../@features/vip/frontend-client/index.html | 81 ++++++++++++++++++- .../frontend-client/src/pages/QuotePage.tsx | 52 +++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/codebase/@features/vip/frontend-client/index.html b/codebase/@features/vip/frontend-client/index.html index 7593eb08..7e9648a8 100644 --- a/codebase/@features/vip/frontend-client/index.html +++ b/codebase/@features/vip/frontend-client/index.html @@ -53,6 +53,85 @@ .vip-personal-note { font-size: 24px; max-width: 100%; } } + /* vip-text-well: prose has tighter measure than the wide container */ + .vip-section-body { max-width: 720px; margin: 0 auto; } + .vip-section-hero .vip-doc-intro { max-width: 640px; } + /* but the option-card grid and the table get the full container width */ + .vip-section-body .vip-pricing, + .vip-section-body .vip-table-wrap { max-width: none; } + /* Salutation spacing */ + .vip-doc-intro .vip-p:first-child { margin-bottom: 6px; } + + /* vip-section-nav */ + .vip-section-nav { + position: fixed; + top: 50%; + right: 28px; + transform: translateY(-50%); + z-index: 10; + padding: 14px 0; + pointer-events: auto; + } + .vip-section-nav-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 14px; + } + .vip-section-nav-link { + display: flex; + align-items: center; + gap: 12px; + text-decoration: none; + color: rgba(212,175,55,0.55); + font-family: 'Menlo', 'Monaco', monospace; + font-size: 10px; + letter-spacing: 0.18em; + text-transform: uppercase; + transition: color 220ms ease, transform 220ms ease; + } + .vip-section-nav-link::before { + content: ''; + display: block; + width: 18px; + height: 1px; + background: currentColor; + opacity: 0.6; + transition: width 280ms ease; + } + .vip-section-nav-item.is-active .vip-section-nav-link { + color: #D4AF37; + } + .vip-section-nav-item.is-active .vip-section-nav-link::before { + width: 40px; + opacity: 1; + } + .vip-section-nav-link:hover { color: #D4AF37; } + .vip-section-nav-index { + flex: 0 0 auto; + opacity: 0.7; + font-variant-numeric: tabular-nums; + } + .vip-section-nav-label { + max-width: 140px; + opacity: 0; + transform: translateX(-4px); + transition: opacity 220ms ease, transform 220ms ease; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .vip-section-nav:hover .vip-section-nav-label, + .vip-section-nav-item.is-active .vip-section-nav-label { + opacity: 1; + transform: translateX(0); + } + @media (max-width: 980px) { + .vip-section-nav { display: none; } + } + /* vip-parallax: section bands, ghost numerals, scroll reveal */ .vip-doc { display: flex; @@ -69,7 +148,7 @@ } .vip-section-inner { width: 100%; - max-width: 760px; + max-width: 920px; margin: 0 auto; position: relative; z-index: 1; diff --git a/codebase/@features/vip/frontend-client/src/pages/QuotePage.tsx b/codebase/@features/vip/frontend-client/src/pages/QuotePage.tsx index 724d1bda..731c1307 100644 --- a/codebase/@features/vip/frontend-client/src/pages/QuotePage.tsx +++ b/codebase/@features/vip/frontend-client/src/pages/QuotePage.tsx @@ -371,7 +371,8 @@ export function QuotePage(): ReactElement { const { intro, sections } = parseDocSections(doc.body); return (
-
+ ({ id: `section-${i + 1}`, label: s.heading }))} /> +
Quote №{state.quote.version} @@ -389,6 +390,7 @@ export function QuotePage(): ReactElement { {sections.map((s, i) => (
@@ -435,6 +437,52 @@ interface MdBlock { rows?: InlineToken[][][]; } +interface SectionNavItem { id: string; label: string } + +function SectionNav({ items }: { items: SectionNavItem[] }): ReactElement { + const [active, setActive] = useState(null); + + useEffect(() => { + if (typeof IntersectionObserver === 'undefined') return; + const nodes = items + .map((it) => document.getElementById(it.id)) + .filter((n): n is HTMLElement => n !== null); + if (nodes.length === 0) return; + const observer = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((e) => e.isIntersecting) + .sort((a, b) => b.intersectionRatio - a.intersectionRatio); + if (visible.length > 0) setActive(visible[0]!.target.id); + }, + { rootMargin: '-30% 0px -55% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] }, + ); + for (const n of nodes) observer.observe(n); + return () => observer.disconnect(); + }, [items]); + + const handleJump = (e: React.MouseEvent, id: string): void => { + e.preventDefault(); + const el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; + + return ( + + ); +} + interface QuoteDocParts { title: string; subtitle: string | null; @@ -780,7 +828,7 @@ const rootStyle: React.CSSProperties = { justifyContent: 'center', padding: '64px 24px 96px', position: 'relative', - fontFamily: "'system-ui', -apple-system, sans-serif", + fontFamily: "'Inter', system-ui, -apple-system, sans-serif", color: G.cream, };